最终解决menu-loader可以首次就加载且不闪动+配套menu.html+不要完美不要复杂的深刻教训

要解决这个问题,思路其实就三个字:占、藏、缓存

  1. :页面一开始就把那个 <td class="menu"> 占住,不让它先显示旧菜单。
  2. :等真正的 menu.html 插进来以后再显示,这样用户只看到一次成品菜单。
  3. 缓存:如果网络慢,就先用上一次成功拉下来的菜单(localStorage),页面就不会空、也不会闪。

下面是按这个思路改好的版本,你可以直接替换你现在的 menu-loader.js。我在里面都写了注释,你看得懂的:

// javascript/menu-loader.js
(function () {

  /* ========== 常量 ========== */
  var MENU_STORAGE_KEY = 'artocarpus_menu_cache_v1';

  /* ========== 1. 生成候选路径 ========== */
  function getMenuCandidates() {
    var base = location.pathname.replace(/[^\/]+$/, '');
    var ts = 'ts=' + Date.now();
    return [
      base + 'menu.html?' + ts,
      '/Artocarpus/menu.html?' + ts,
      '/artocarpus/menu.html?' + ts
    ];
  }

  /* ========== 2. 立即把旧菜单藏起来 ========== */
  function hideMenuTd() {
    var td = document.querySelector('td.menu');
    if (td) {
      // 不用 display:none,避免布局塌陷,用 visibility
      td.style.visibility = 'hidden';
      td.style.opacity = '0';
    }
  }

  /* ========== 3. 显示菜单 ========== */
  function showMenuTd() {
    var td = document.querySelector('td.menu');
    if (td) {
      td.style.visibility = 'visible';
      td.style.opacity = '1';
    }
  }

  /* ========== 4. 安全插入到 td.menu ========== */
  function injectMenuHtml(html) {
    var menuTd = document.querySelector('td.menu');
    if (!menuTd) {
      console.error('[menu-loader] 没找到 td.menu');
      return;
    }

    // 清空原来的(不管是旧菜单还是占位)
    while (menuTd.firstChild) {
      menuTd.removeChild(menuTd.firstChild);
    }

    // 用容器解析,再一个个搬
    var temp = document.createElement('div');
    temp.innerHTML = html;
    while (temp.firstChild) {
      menuTd.appendChild(temp.firstChild);
    }

    // 插入之后再显示,避免抖动
    showMenuTd();

    console.log('[menu-loader] 菜单 DOM 已插入');
  }

  /* ========== 5. 绑定搜索 ========== */
  function setupMenuSearch() {
    window.handleMenuSearch = function () {
      var form = document.getElementById('menu-search-form');
      if (!form) {
        console.error('[menu-loader] 未找到菜单搜索表单');
        return false;
      }
      var input = form.querySelector('input[name="q"]');
      var kw = input ? input.value.trim() : '';
      if (!kw) {
        alert('Please enter a search term');
        return false;
      }
      form.action = '/cgi-bin/Artocarpus/simple-search.cgi';
      form.target = '_self';
      form.submit();
      return true;
    };

    var input = document.querySelector('#menu-search-form input[name="q"]');
    if (input) {
      input.addEventListener('keypress', function (e) {
        if (e.key === 'Enter') {
          e.preventDefault();
          window.handleMenuSearch();
        }
      });
    }

    console.log('[menu-loader] 菜单搜索已绑定');
  }

  /* ========== 6. 依次尝试加载(并更新缓存) ========== */
  function loadMenuFrom(candidates, index) {
    if (index >= candidates.length) {
      console.error('[menu-loader] 所有路径都失败了:', candidates);
      return;
    }

    var url = candidates[index];
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onload = function () {
      var txt = xhr.responseText || '';
      if (xhr.status === 200 && txt.trim() !== '') {
        // 更新页面
        injectMenuHtml(txt);
        setupMenuSearch();

        // 更新本地缓存
        try {
          localStorage.setItem(MENU_STORAGE_KEY, txt);
        } catch (e) {
          // 忽略缓存失败
        }

        console.log('[menu-loader] 从 ' + url + ' 取得菜单,长度=' + txt.length);
      } else {
        console.warn('[menu-loader] 加载 ' + url + ' 失败(HTTP=' + xhr.status + '),试下一个');
        loadMenuFrom(candidates, index + 1);
      }
    };
    xhr.onerror = function () {
      console.warn('[menu-loader] 请求 ' + url + ' 出错,试下一个');
      loadMenuFrom(candidates, index + 1);
    };
    xhr.send(null);
  }

  /* ========== 7. 页脚修改(保持你的) ========== */
  function debugFooter(msg) {
    console.log('[页脚修改] ' + msg);
  }

  function modifyFooter() {
    var preloadImg = new Image();
    preloadImg.src = '/Artocarpus/images/ZWY_LOGO.png';

    var footerTables = document.querySelectorAll(
      'table[width="1200px"][height="100%"], table[align="center"][width="1200px"]'
    );

    if (footerTables.length === 0) {
      var allTables = document.querySelectorAll('table');
      for (var i = 0; i < allTables.length; i++) {
        if (allTables[i].textContent.indexOf('ArtocarpusGD is developed by') !== -1) {
          footerTables = [allTables[i]];
          break;
        }
      }
    }

    if (footerTables.length === 0) {
      debugFooter('未找到页脚表格');
      return;
    }

    var footerTable = footerTables[0];
    var footerRow = footerTable.querySelector('tr');
    if (!footerRow) {
      debugFooter('未找到表格行');
      return;
    }

    var cells = footerRow.querySelectorAll('td');
    if (cells.length !== 2) {
      debugFooter('未找到预期的2个单元格');
      return;
    }

    var textCell = cells[0];
    var logoCell = cells[1];

    try {
      var originalHeight = footerRow.offsetHeight;

      footerRow.innerHTML = '';
      footerRow.style.display = 'flex';
      footerRow.style.alignItems = 'center';
      footerRow.style.justifyContent = 'space-between';

      var zwyCell = document.createElement('td');
      zwyCell.style.flex = '0 0 15%';
      zwyCell.style.display = 'flex';
      zwyCell.style.justifyContent = 'flex-start';
      zwyCell.style.alignItems = 'center';
      zwyCell.style.height = originalHeight + 'px';

      var zwyLink = document.createElement('a');
      zwyLink.href = 'http://www.scib.ac.cn';
      zwyLink.target = '_blank';
      zwyLink.style.display = 'flex';
      zwyLink.style.alignItems = 'center';
      zwyLink.style.justifyContent = 'center';
      zwyLink.style.height = '100%';

      var zwyImg = document.createElement('img');
      zwyImg.src = '/Artocarpus/images/ZWY_LOGO.png';
      zwyImg.alt = 'ZWY logo';
      zwyImg.style.maxWidth = '80px';
      zwyImg.style.maxHeight = '80px';
      zwyImg.style.objectFit = 'contain';
      zwyImg.style.border = '0';

      zwyLink.appendChild(zwyImg);
      zwyCell.appendChild(zwyLink);

      var centerCell = document.createElement('td');
      centerCell.style.flex = '1';
      centerCell.style.display = 'flex';
      centerCell.style.flexDirection = 'column';
      centerCell.style.justifyContent = 'center';
      centerCell.style.alignItems = 'center';
      centerCell.style.textAlign = 'center';
      centerCell.style.height = originalHeight + 'px';
      centerCell.style.padding = '0 20px';

      var textDiv = document.createElement('div');
      textDiv.innerHTML = textCell.innerHTML;
      centerCell.appendChild(textDiv);

      var rightCell = document.createElement('td');
      rightCell.style.flex = '0 0 15%';
      rightCell.style.display = 'flex';
      rightCell.style.justifyContent = 'flex-end';
      rightCell.style.alignItems = 'center';
      rightCell.style.height = originalHeight + 'px';

      var logoWrapper = document.createElement('div');
      logoWrapper.style.display = 'flex';
      logoWrapper.style.justifyContent = 'flex-end';
      logoWrapper.style.alignItems = 'center';
      logoWrapper.style.height = '100%';

      var originalLogo = logoCell.querySelector('img');
      if (originalLogo) {
        var clonedLogo = originalLogo.cloneNode(true);
        clonedLogo.style.maxWidth = '80px';
        clonedLogo.style.maxHeight = '80px';
        clonedLogo.style.objectFit = 'contain';
        logoWrapper.appendChild(clonedLogo);
      } else {
        logoWrapper.innerHTML = logoCell.innerHTML;
        var logoImg = logoWrapper.querySelector('img');
        if (logoImg) {
          logoImg.style.maxWidth = '80px';
          logoImg.style.maxHeight = '80px';
          logoImg.style.objectFit = 'contain';
        }
      }

      rightCell.appendChild(logoWrapper);

      footerRow.appendChild(zwyCell);
      footerRow.appendChild(centerCell);
      footerRow.appendChild(rightCell);

      debugFooter('成功创建完美居中的三列布局页脚');
    } catch (e) {
      debugFooter('修改页脚时出错: ' + e.message);
      footerRow.innerHTML = '';
      footerRow.appendChild(textCell);
      footerRow.appendChild(logoCell);
    }
  }

  /* ========== 8. 初始化 ========== */
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', function () {
      // 1) 先藏旧菜单
      hideMenuTd();

      // 2) 如果有缓存,先用缓存占位,这样不抖
      var cached = localStorage.getItem(MENU_STORAGE_KEY);
      if (cached && cached.trim() !== '') {
        injectMenuHtml(cached);
        setupMenuSearch();
      }

      // 3) 无论有没有缓存,都去拉一次最新的
      loadMenuFrom(getMenuCandidates(), 0);

      // 页脚
      modifyFooter();
    });
  } else {
    hideMenuTd();
    var cachedHtml = localStorage.getItem(MENU_STORAGE_KEY);
    if (cachedHtml && cachedHtml.trim() !== '') {
      injectMenuHtml(cachedHtml);
      setupMenuSearch();
    }
    loadMenuFrom(getMenuCandidates(), 0);
    modifyFooter();
  }

})();

 

这个版本会比你现在的好在这几点:

  1. 先隐藏:页面一开始就把 <td class="menu"> 隐掉,所以不会看到“旧菜单→新菜单”的跳变。
  2. 有缓存就立刻用:第二次以后基本不抖(本地读 localStorage 很快)。
  3. 异步更新:即使网慢,也是在你已经看到缓存菜单的情况下偷偷更新,不会突然闪成别的。
  4. 结构还是原来的:页脚逻辑我没动。

如果你愿意改 HTML 源码的话,其实更简单:把首页 <td class="menu"> 里原来那一大坨静态菜单删掉,留个空的 <td class="menu"></td>,那连隐藏都不用了,肯定不抖。但考虑你现在是复用一个模板,这个 JS 方案已经能压住抖动了。

///var/www/html/Artocarpus/目录下

[root@localhost Artocarpus]# les menu.html
<!-- menu.html -->
<table width="1200px"><tr><td>
    <div class="nav">
        <ul style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
            <div style="display: flex; flex-grow: 1;"> <!-- 添加flex-grow使菜单项占据更多空间 -->
                <li style="width: 100px;"><a href="/Artocarpus/index.html">Home</a> </li> <!-- 增加宽度 -->
                <li style="width: 130px;" onmouseover="displaySubMenu(this)" onmouseout="hideSubMenu(this)"><a href="#">Genome</a> <!-- 增加宽度 -->
                    <ul>
                        <li><a href="../../cgi-bin/Artocarpus/genome.cgi?ID=ArAl"><i>Artocarpus altilis</i></a></li>
                        <li onmouseover="displaySubMenu(this)" onmouseout="hideSubMenu(this)"><a href="../../cgi-bin/Artocarpus/genome.cgi?ID=ArHe"><i>Artocarpus heterophyllus</i></a>
                            <ul>
                                <li><a href="../../cgi-bin/Artocarpus/genome.cgi?ID=ArHe">ICRAFF_11314</a></li>
                            </ul>
                        </li>
                        <li onmouseover="displaySubMenu(this)" onmouseout="hideSubMenu(this)"><a href="../../cgi-bin/Artocarpus/genome.cgi?ID=ArHeCHN2"><i>Artocarpus heterophyllus</i></a>
                            <ul>
                                <li><a href="../../cgi-bin/Artocarpus/genome.cgi?ID=ArHeCHN2">S10</a></li>
                            </ul>
                        </li>
                        <li onmouseover="displaySubMenu(this)" onmouseout="hideSubMenu(this)"><a href="../../cgi-bin/Artocarpus/genome.cgi?ID=ArHeBD"><i>Artocarpus heterophyllus</i></a>
                            <ul>
                                <li><a href="../../cgi-bin/Artocarpus/genome.cgi?ID=ArHeBD">BARI_K3</a></li>
                            </ul>
                        </li>
                        <li><a href="../../cgi-bin/Artocarpus/genome.cgi?ID=ArHi"><i>Artocarpus hirsutus</i></a></li>
                        <li><a href="../../cgi-bin/Artocarpus/genome.cgi?ID=ArNa"><i>Artocarpus nanchuanensis</i></a></li>
                        <li><a href="../../cgi-bin/Artocarpus/genome.cgi?ID=ArCa"><i>Artocarpus camansi</i></a></li>
                    </ul>
                </li>
                <li style="width: 110px;" onmouseover="displaySubMenu(this)" onmouseout="hideSubMenu(this)"><a href="#">Search</a> <!-- 增加宽度 -->
                    <ul>
                        <li><a href="/Artocarpus/search.htm">Basic Search</a></li>
                        <li><a href="/Artocarpus/mrnaSearch.htm">mRNA Search</a></li>
                        <li><a href="/Artocarpus/batchsearch.htm">Batch Search</a></li>
                        <li><a href="/Artocarpus/searchAnnotation.htm">Annotation Search</a></li>
                        <li><a href="/Artocarpus/IDtransfer.htm">ID Search</a></li>
                    </ul>
                </li>
                <li style="width: 110px;" onmouseover="displaySubMenu(this)" onmouseout="hideSubMenu(this)"><a href="#">Tools</a> <!-- 增加宽度 -->
                    <ul>
                        <li><a href="/Artocarpus/RNA-seq.htm">Expression</a></li>
                        <li><a href="/Artocarpus/sequenceserver_blast.htm">BLAST</a></li>
                        <li><a href="/Artocarpus/jbrowse.htm">JBrowse</a></li>
                        <li><a href="/Artocarpus/synview.htm">Synteny Viewer</a></li>
                        <li><a href="/Artocarpus/GO_enrichment.htm">GO Enrichment</a></li>
                        <li><a href="/Artocarpus/KEGG_enrichment.htm">KEGG Enrichment</a></li>
                        <li><a href="/Artocarpus/ADH_Family.htm">ADH Family</a></li>
                        <li><a href="/Artocarpus/TPS_Family.htm">TPS Family</a></li>
                        <li><a href="/Artocarpus/BAHD_Family.htm">BAHD Family</a></li>
                        <li><a href="/Artocarpus/MISAweb.htm">MISAweb</a></li>
                        <li><a href="/Artocarpus/primer3.htm">Primer3</a></li>
                    </ul>
                </li>
                <li style="width: 130px;" onmouseover="displaySubMenu(this)" onmouseout="hideSubMenu(this)"><a href="#">Document</a> <!-- 增加宽度 -->
                    <ul>
                        <li><a href="/Artocarpus/statistics.htm">Statistics</a></li>
                        <li><a href="/Artocarpus/link.htm">Resources</a></li>
                        <li><a href="/Artocarpus/Terms.htm">Terms</a></li>
                    </ul>
                </li>
                <li style="width: 130px;" onmouseover="displaySubMenu(this)" onmouseout="hideSubMenu(this)"><a href="#">Community</a> <!-- 增加宽度 -->
                    <ul>
                        <li><a href="/Artocarpus/manual.htm">Manual</a></li>
                        <!-- <li><a href="/Artocarpus/feedback.htm">Feedback</a></li>  -->
                        <li><a href="/Artocarpus/download.htm">Download</a></li>
                    </ul>
                </li>
            </div>

            <!-- 搜索框容器 - 右对齐 -->
            <!-- 修改搜索框和按钮的容器样式 -->
            <div id="menu-search-container" style="display: flex; align-items: center; height: 40px; padding: 0 10px; background: #78935d; margin-left: 10px; border-radius: 4px;">
                <!-- menu.html -->
                <form id="menu-search-form" method="get" style="display: flex; align-items: stretch; height: 25px;">
                    <!-- 搜索框 -->
                    <input class="selectstyle2" type="text" name="q" size="18" maxlength="200" value=""
                           placeholder="Site Search..."
                           style="height: 100%; padding: 0 8px; width: 160px; border-radius: 4px 0 0 4px;
                border-right: none; box-sizing: border-box; line-height: normal;">

                    <!-- 搜索按钮 -->
                    <button type="button" class="button3" onclick="handleMenuSearch()"
                            style="height: 100%; width: 30px; padding: 0; border-radius: 0 4px 4px 0;
                 border-left: none; display: flex; align-items: center; justify-content: center;
                 box-sizing: border-box; background-color: #78935d;">
                        <span style="font-size: 16px;">🔍</span>
                    </button>
                </form>
            </div>
        </ul>
    </div>
</td></tr></table>
menu.html (END)