Gerrit是开源社区广泛使用的代码评审工具,它的Diff页面会将一次提交中所有变更文件依次渲染出来。当一个提交涉及几十甚至上百个文件时,评审者如果想定位某个文件的差异,只能靠浏览器自带的查找或反复滚动,体验很差。为Diff页面增加一个侧边栏导航,列出全部变更文件并支持点击跳转、滚动联动高亮,能显著提升评审效率。本文结合jQuery,剖析这一侧边栏导航的完整实现思路。
一、分析Gerrir Diff页面的DOM结构并动态生成侧边栏
实现侧边栏的第一步是搞清楚Diff页面的DOM结构。Gerrit(以经典的GWT界面或PolYGerrit之前的版本为例)通常会给每个文件的差异区块一个可识别的容器,常见的类名包括.section、.diffBox或者带有data-path属性的元素。我们先通过jQuery选择器把这些区块收集起来,再为每个区块提取文件路径作为导航文字。
下面的代码演示了如何扫描页面并动态插入一个固定在右侧的侧边栏:
// 收集所有文件差异区块
var $sections = $('.diffBox, .section');
// 创建侧边栏容器并插入到body
var $sidebar = $('<div id="diff-nav-sidebar"></div>').css({
position: 'fixed',
right: '10px',
top: '80px',
width: '220px',
maxHeight: '70vh',
overflowY: 'auto',
background: '#fff',
border: '1px solid #ccc',
zIndex: 9999,
fontSize: '12px'
});
$('body').append($sidebar);
// 为每个差异区块生成导航项
$sections.each(function (index) {
var path = $(this).data('path') || $(this).find('.pathText').text() || ('文件 ' + (index + 1));
$(this).attr('id', 'diff-section-' + index); // 保证可跳转
$sidebar.append(
$('<a></a>')
.attr('href', '#diff-section-' + index)
.text(path)
.css({ display: 'block', padding: '4px 8px', textDecoration: 'none', color: '#15c' })
);
});
这段代码的关键点有两个:一是给每个差异区块设置唯一的id,作为锚点跳转的依据;二是文件路径的提取要考虑降级,不同版本的Gerrit DOM结构略有差异,优先读data-path属性,找不到时再从内部文本节点兜底。侧边栏本身使用position: fixed固定在视口右侧,并通过overflow-y: auto保证文件过多时自身可滚动。
二、点击跳转与滚动联动高亮
点击导航项时,直接用锚点跳转会瞬间定位,体验生硬,而且Gerrit页面顶部通常有固定工具栏,锚点定位后目标可能被遮挡。更好的做法是用animate做平滑滚动,并预留顶部偏移量:
$sidebar.on('click', 'a', function (e) {
e.preventDefault();
var targetId = $(this).attr('href');
var offset = $(targetId).offset().top - 70; // 70为固定头部高度
$('html, body').animate({ scrollTop: offset }, 300);
});
滚动联动高亮是侧边栏导航的灵魂:当用户手动滚动页面时,侧边栏中对应的文件项应该自动高亮。实现思路是监听scroll事件,遍历所有差异区块,找到当前滚动视口顶部所在的那个区块,然后更新高亮状态:
var sectionTops = [];
function cachePositions() {
sectionTops = $sections.map(function () {
return $(this).offset().top;
}).get();
}
$(window).on('scroll', function () {
var scrollTop = $(window).scrollTop() + 100; // 加一点余量
var activeIndex = 0;
for (var i = 0; i < sectionTops.length; i++) {
if (sectionTops[i] <= scrollTop) activeIndex = i;
}
$sidebar.find('a').removeClass('active')
.eq(activeIndex)
.addClass('active')
.css({ background: '#e8f0fe', fontWeight: 'bold' });
$sidebar.find('a').css('background', '').not('.active');
// 让当前高亮项保持在侧边栏可视区域内
var $active = $sidebar.find('a.active');
if ($active.length) {
var barTop = $sidebar.scrollTop();
var aTop = $active.position().top;
if (aTop < 0 || aTop > $sidebar.height() - 30) {
$sidebar.scrollTop(barTop + aTop - $sidebar.height() / 2);
}
}
});
这里把所有区块的offset().top缓存到数组中,避免每次滚动都重新查询DOM,这是非常重要的性能习惯。遍历采用线性扫描即可满足需求;如果文件数量极大,也可以换成二分查找。高亮之后还要处理侧边栏自身的滚动跟随,确保高亮项不会滚出侧边栏的可视区域。
三、防抖优化与动态加载场景的兼容处理
滚动事件触发频率非常高,直接在回调里做DOM操作会造成页面卡顿。除了缓存位置信息外,还应该使用防抖或节流。同时要注意,Gerrit的Diff页面有些文件内容是懒加载的,用户滚动到附近时才渲染diff表格,这会导致区块位置不断变化,需要在合适的时机重新缓存位置:
function debounce(fn, wait) {
var timer = null;
return function () {
var context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, wait);
};
}
// 防抖处理滚动回调
$(window).on('scroll', debounce(function () {
highlightCurrentSection();
}, 100));
// 监听Gerrit动态插入diff内容后重新计算位置
var observer = new MutationObserver(debounce(function () {
cachePositions();
}, 300));
observer.observe($('#mainContent')[0] || document.body, {
childList: true,
subtree: true
});
// 窗口尺寸变化时同样需要重算
$(window).on('resize', debounce(cachePositions, 200));
MutationObserver可以捕获Gerrit懒加载新差异内容后的DOM变化,在变化平息后重新调用cachePositions刷新位置缓存,保证联动高亮始终准确。如果目标浏览器较老不支持MutationObserver,可以退而求其次在滚动回调里判断区块数量是否变化,再触发重算。
最后还有几个实用细节值得注意:一是侧边栏可以按目录分组展示文件,利用路径中的斜杠做层级折叠;二是可以通过$.cookie或localStorage记住侧边栏的展开与收起状态;三是如果文件路径过长,用text-overflow: ellipsis截断并加title属性提示完整路径。这些交互叠加起来,一个体验接近原生IDE的Diff导航就完成了。同样的思路也适用于GitLab、自研Code Review平台或任何长列表差异展示页面,核心始终是定位、跳转、联动这三步。
jQueryGerrit Code ReviewDiff侧边栏修改时间:2026-08-31 07:28:58