背景
在日常的开发和测试工作中,可能会经常遇到团队成员对当前所处环境真实性的疑虑。例如,开发人员和测试人员经常会问:“我们现在是在测试环境中吗?”或者“为什么测试环境的数据看起来这么真实?”这些问题不仅增加了内部沟通的成本,还可能导致操作上的犹豫不决,进而影响工作效率。
目标
为了使团队成员能够轻松识别当前系统所处的环境,从而减少混淆和错误操作的风险,我们深入探索并实现了一种中后台系统的环境标识方案。这一方案旨在清晰地区分测试环境、沙箱环境以及线上环境,确保每个用户都能一目了然地了解自己正在使用的系统环境。
准备工作
当谈及实现的是中后系统通用的环境标识时,不知道大家有没有人跟我一样第一反应可能是通过一个通用接口来实现,只需根据不同的项目传递相应的参数以获取所需的数据。然而深入思考后会发现,这种方法实现的可能性很小。原因在于:一个系统通常会调用多个不同的接口,而一旦配置了代理,通用接口就无法获取到用户特定的代理设置。更复杂的是:不同的接口可能使用不同的代理配置,这进一步增加了实现的难度。
因此,就需要转变思路,考虑是否可以让Nginx在每个接口的响应头中添加一个特定的标签(tag),以标识当前所处的Docker环境。受到这个想法的启发,因此就出现了以下的方案:
1. 响应头添加标识: 通过在Nginx配置中为每个接口添加一个响应头,该响应头包含一个标识当前环境的标签。这样,无论用户如何配置代理,每个接口的响应都能准确反映出其所属的环境。
2. 前端检测与显示: 前端应用将检测这些响应头中的标签,并根据这些信息在界面上显示相应的环境标识,从而让用户能够直观地识别当前所处的环境。
3. 灵活的配置选项: 为了适应不同的使用场景,我们的方案还提供了一系列的配置选项,包括自定义DOM类名或ID选择器、自定义样式以及控制环境标识的显示逻辑。
方案设计
设计思路是,在Nginx中为每个响应添加一个环境标识的响应头,这样前端应用就可以根据这个标识来显示当前的环境。这种方法的优势在于它的简洁性和高效性,因为它直接在服务器端解决了环境识别的问题,而不需要后端进行复杂的逻辑判断。
关于配置
任何强大的功能都离不开灵活的配置,当然环境标识功能也不例外。为了使这个功能能够适应各种不同的使用场景,专门提供了一些基本的配置选项,这些选项可以通过API进行调整。以下是所有的配置项:
domain: 需要配置的检测环境的域名,支持多个,取的配置每个域名的第一个接口为基准(原因 :刷新页面有可能接口的执行顺序不一样 ,假定不同域名的 接口 走的环境不一样 ,用户刷新出现的环境标识会不同,影响体验 )
insertDefaultDomClassOrID:如果你想自定义环境标识的位置,可以通过这个配置项指定一个DOM类名或ID选择器,我们会将环境标识插入到对应的DOM元素中。
customStyle:提供了丰富的样式自定义选项,包括宽度、高度、字体颜色、背景色等,以确保环境标识能够与你的界面设计无缝融合。
isShowOnline:控制是否在线上环境展示后端环境标识的开关。默认为false,以避免在生产环境中暴露不必要的信息。
isCollapseHide:在侧边菜单折叠时是否隐藏环境标识,以保持界面的整洁。
isShowFrontEnvironment:控制是否展示前端环境标识,这对于调试前端环境非常有用。
insertDefaultDomClassOrID:如果你想自定义环境标识的位置,可以通过这个配置项指定一个DOM类名或ID选择器,我们会将环境标识插入到对应的DOM元素中。
customStyle:提供了丰富的样式自定义选项,包括宽度、高度、字体颜色、背景色等,以确保环境标识能够与你的界面设计无缝融合。
isShowOnline:控制是否在线上环境展示后端环境标识的开关。默认为false,以避免在生产环境中暴露不必要的信息。
isCollapseHide:在侧边菜单折叠时是否隐藏环境标识,以保持界面的整洁。
isShowFrontEnvironment:控制是否展示前端环境标识,这对于调试前端环境非常有用。
通过这些配置项,你可以精细地控制环境标识的行为,使其完全符合你的项目需求。这种灵活性是我们方案的一大亮点,也是它能够被广泛采用的关键因素之一。
关于兼容
1. 切换路由弹窗消失
在比较复杂的中后台系统中,可能会遇到这样的情况:大部分接口指向测试环境,但少数接口可能配置为指向沙箱环境。这可能导致在点击环境标识时,弹出的弹窗中显示了与当前环境不一致的接口信息。为了解决这个问题,我们采取了以下措施:
自动隐藏弹窗: 当用户切换到新的路由时,我们确保之前显示的弹窗能够自动消失。这有助于防止信息混淆,并确保用户在新路由下能够看到正确的环境信息,从而提供更加清晰和准确的环境反馈。
清除旧环境数据: 除了自动隐藏弹窗外,我们还清除了上一个路由的环境数据。这一步骤是必要的,以防止旧数据对用户的决策造成干扰。通过清除旧数据,我们能够确保每次路由变更时,用户都能够得到最新和最相关的环境信息,从而提高系统的可用性和用户的决策效率。
那么在切换路由的时候肯定是希望弹窗也消失的,并且上一个路由的数据也需要清除
兼容方案
为了实现上述功能,我们监听了路由的变化。每当路由发生变化时,我们不仅隐藏弹窗,还清除了与之相关的数据。这样,无论用户如何切换路由,都能确保他们看到的是最新和最准确的环境信息
核心代码
// 监听路由切换重置弹窗
window.onhashchange = function () {
// 当路由发生变化时执行的代码
const newHash = window.location.hash
if (newHash) {
$modal_dissting_environment = document.querySelector('.modal-distring-environment')
// 切换路由的时候重置一下弹窗的显示,防止有问题
if ($modal_dissting_environment) {
$modal_dissting_environment.style.display = 'none'
}
// 只需要当前菜单的接口
clickHistory = []
isModalVisible = false
}
}
2. 刷新页面弹窗出现
最初,我们采用了一种直接的方法来展示环境标识弹窗:当检测到当前路由所对应的接口环境与我们的预期不一致时,我们通过innerHTML
将这些接口的详细信息添加到弹窗中。随后,通过调整弹窗的display
属性来控制其显示与隐藏状态。
// 点击生成弹窗
const createModalData = (current, url) => {
let $modal_dissting_environment = document.querySelector('.modal-distring-environment')
// 将满足条件的内容添加到点击历史记录数组中
// 注意:这里有current == null,代表返回没有global-tag这个字段,代表是线上环境
if (currentEnvir != current) {
const content = '接口' + extractUrlParts(url) + '与当前环境不一致' + '<br>';
if (!clickHistory.includes(content)) {
clickHistory.push(content);
$modal_dissting_environment.innerHTML = clickHistory?.join('');
}
}
}
这种方法存在一些问题:尽管我们初始化时将弹窗的display设置为none,但如果弹窗内有内容,其visibility属性可能会变为visible,导致即使display属性被设置为none也无法将其隐藏。
优化后的写法
const createModalData = (current, url) => {
$modal_dissting_environment = document.querySelector('.modal-distring-environment')
// 将满足条件的内容添加到点击历史记录数组中
// 注意:这里有current == undefinednull,代表返回没有global-tag这个字段,代表是线上环境
if (window.localStorage.getItem('currentEnvironmentTag') && window.localStorage.getItem('currentEnvironmentTag') != current) {
const content = '接口' + extractUrlParts(url) + '与当前环境不一致' + '<br>'
if (!clickHistory.includes(content)) {
clickHistory.push(content)
}
}
}
// 监听点击事件,用来显示和隐藏弹窗,因为dom更新有时候延迟,所以需要监听
document.addEventListener('click', function (event) {
// 使用正确的选择器类名,这里假设 '.pc-distring-environment' 是触发显示/隐藏的元素
if (event.target.closest('.pc-distring-environment')) {
const modalDisstingEnvironment = document.querySelector('.modal-distring-environment');
const hasContent = clickHistory.length > 0; // 检查是否有内容
if (modalDisstingEnvironment) {
// 仅在有内容时切换显示状态
if (hasContent) {
isModalVisible = !isModalVisible;
// 根据当前状态设置 display 和 visibility
modalDisstingEnvironment.style.display = isModalVisible ? 'block' : 'none';
modalDisstingEnvironment.style.visibility = isModalVisible ? 'visible' : 'hidden';
} else {
// 如果没有内容,确保弹窗不显示
modalDisstingEnvironment.style.display = 'none';
modalDisstingEnvironment.style.visibility = 'hidden';
}
// 仅在切换为可见状态时设置 innerHTML
if (isModalVisible) {
modalDisstingEnvironment.innerHTML = clickHistory.join('');
}
}
}
});
3. MutationObserver:解决DOM更新滞后问题
当观察到DOM的刷新相对于JavaScript执行有所延迟时,我们很自然地会想到使用“定时器”这一工具。尽管定时器在某些情况下确实能提供便利,但它在代码的优雅性和通用性方面存在不足。特别是在开发适用于所有后台项目的技术方案时,由于无法预知页面刷新和接口响应所需的具体时间,定时器显然不是最佳选择。这时,"MutationObserver"便成为了一个更合适的解决方案。
MutationObserver是浏览器环境中的一种JavaScript API,专门用于监听DOM树中指定节点及其子节点的变化。当监测到节点内容、属性或子节点结构发生变动时,MutationObserver能够立即察觉并触发相应的回调函数,从而提供了一种高效且精确的方式来响应DOM的变更。
看到这个解释有没有感觉正好是你需要的API
定时器写法
$modalDissting.style.cssText = styleObjToString(commonStyle)
$parentEl.appendChild($modalDissting)
setTimeout(() => {
observeDom(curentdom, $parentEl);
}, 0)
MutationObserver写法
const observeDom = (currentDomSelector, ele) => {
const observerDom = new MutationObserver((mutations) => {
// 使用 `forEach` 避免在循环中调用 `disconnect`
mutations.forEach((mutation) => {
if (mutation && !document.querySelector('.pc-distring-environment')) {
......
observerDom.disconnect(); //停止观察
}
});
});
const observerConfig = { attributes: true, childList: true, subtree: true };
observerDom.observe(document.body, observerConfig);
......
observerDom.takeRecords(); // 清空记录,避免重复执行
}
};
虽然看起来使用MutationObserver的代码远远比定时器的代码行数要多,但是需要根据性能,准确性等方面考虑的话,MutationObserver无疑是更现代和高效的方法
4. 兼容泛域名
在转转内部,前端团队与工程效率组的合作成果之一便是实现了泛域名技术,这项技术允许我们无需代理即可访问线下或沙箱环境。例如,通过泛域名,我们可以访问如下URL
https://a-xxx.test.yyy.com/route/index
这里的系统域名是 https://a.yyy.com,而 xxx 代表的是申请的Docker机器名称。根据子域名中的关键字,我们可以识别出环境类型:test 表示测试环境,sand 则代表沙箱环境。
尽管有了泛域名的便利,许多用户仍然不确定自己是否真正访问了测试环境。为了解决这一疑虑,我们决定采取一种更明确的方式,向用户展示当前所处的环境。
由于用户可能会选择使用泛域名进行访问,新npm包也需要对此进行兼容。在实际应用中,用户会自行传入配置域名,例如 a.yyy.com。但在泛域名访问时,接口的域名可能会变成 a-xxx.test.yyy.com,这就需要我们在处理时能够准确识别当前环境。
为了兼容泛域名,采取了以下方案:通过获取所有请求接口的 responseUrl,我们在过滤接口时需要更加严谨。我们不能简单地通过包含关系来判断,而是要先提取主机域名,然后在此基础上进行过滤。以下是我们的核心代码实现,我们将对其进行优化,以确保其高效性和准确性。
兼容方案
因为是会获取所有请求接口的responseUrl,所以在过滤接口的时候需要相对严谨一点,不能直接判断包不包含,因此会先拿到主机域名,然后在主机域名的基础上再去过滤,以下是核心代码
核心代码
const transformSubdomain = (originalUrl, domain) => {
//获取主机域名
let hostname = new URL(originalUrl).hostname
// 用于匹配以连字符(-)开始,后跟一个或多个非点号字符([^.]+)
let modifiedUrl = originalUrl.replace(/-[^.]+\.[^.]*/, '')
return hostname.includes('-') ? modifiedUrl : originalUrl
}
5. Iframe内嵌页面
因为大多数系统会用iframe内嵌别人的系统页面,所以需要考虑环境标识在iframe里面的情况
兼容方案
嵌入页面需要判断window.self !== window.top时,不去监测接口即可
最终效果图
总结与展望
好消息!中后台环境标识功能已经成功落地
经过不懈努力,中后台团队的核心后台项目已经接入这一新功能——中后台系统线上线下环境标识。受到了测试和研发同学的好评。
持续改进:推动全面接入与优化
目前,我们正积极推动其他业务线接入这一环境标识能力。在推广过程中,我们收到了来自不同业务团队的宝贵反馈和特定优化。经过认真评估这些优化,进行了迭代开发,以满足更广泛的业务场景。
我们承诺将持续优化这一功能,确保它能够适应不断变化的业务需求,并为所有用户提供更加多面和可靠的支持。随着更多业务线的接入,我们期待环境标识功能能够逐步落地到所有业务团队。