SharePoint 的导航选项

本文介绍 SharePoint 中启用了 SharePoint Publishing 的导航选项网站。 导航的选择和配置会显著影响 SharePoint 中网站的性能和可伸缩性。 SharePoint 发布网站模板应仅在集中门户需要时才使用,并且仅应在特定网站上启用发布功能,并且仅在需要时才启用,因为它在使用不当时可能会影响性能。

注意

如果使用新式 SharePoint 导航选项(如大型菜单、级联导航或中心导航),则本文不适用于您的网站。 新式 SharePoint 网站体系结构利用更平展的网站层次结构和中心辐射型模型。 这允许实现许多不需要使用 SharePoint 发布功能的方案。

导航选项概述

导航提供程序配置可能会显著影响整个网站的性能,因此必须仔细考虑选择可有效缩放 SharePoint 网站要求的导航提供程序和配置。 有两个现成的导航提供程序,以及自定义导航实现。

如果为网站启用结构化导航缓存,则第一个选项“结构导航”是经典 SharePoint 网站中推荐的导航选项。 此导航提供程序显示当前网站下方的导航项,以及当前网站及其同级网站(可选)。 它提供其他功能,例如安全修整和站点结构枚举。 如果禁用缓存,这将对性能和可伸缩性产生负面影响,并且可能会受到限制。

第二个选项 “托管 (元数据) 导航”表示使用托管元数据术语集的导航项。 除非需要,否则建议禁用安全修整。 安全修整作为此导航提供程序的安全默认设置启用;但是,许多网站不需要安全修整的开销,因为导航元素通常对于网站的所有用户都是一致的。 使用禁用安全修整的建议配置,此导航提供程序不需要枚举站点结构,并且高度可缩放,对性能造成可接受的影响。

除了现成的导航提供程序之外,许多客户还成功实现了替代自定义导航实现。 请参阅本文中的搜索驱动的客户端脚本

SharePoint 导航选项的优缺点

下表总结了每个选项的优缺点。

结构导航 托管导航 搜索驱动的导航 自定义导航提供程序
优点:

易于维护
安全修整
内容更改后 24 小时内自动更新
优点:

易于维护
优点:

安全修整
添加站点时自动更新
快速加载时间和本地缓存导航结构
优点:

更广泛的可用选项选择
正确使用缓存时快速加载
许多选项非常适合响应式页面设计
缺点:

如果禁用缓存,则影响性能
受限制的约束
缺点:

未自动更新以反映网站结构
如果启用安全修整 或导航结构复杂,会影响性能
缺点:

无法轻松订购站点
需要自定义母版页 () 所需的技术技能
缺点:

需要自定义开发
需要存储的外部数据源/缓存,例如 Azure

最适合站点的选项取决于站点要求和技术能力。 如果你想要一个易于配置的导航提供程序,以便在内容更改时自动更新,则 启用缓存的 结构化导航是一个不错的选择。

注意

通过将整体网站结构简化为更平整的非分层结构,应用与新式 SharePoint 网站相同的原则可提高性能并简化移动到新式 SharePoint 网站的过程。 这意味着,与将数百个网站 (子网站) 的单个网站集不同,更好的方法是让多个网站集 (子网站) 很少。

分析 SharePoint 中的导航性能

SharePoint 页面诊断工具是 Microsoft Edge 和 Chrome 浏览器的浏览器扩展,用于分析 SharePoint 新式门户和经典发布网站页面。 此工具仅适用于 SharePoint,不能在 SharePoint 系统页面上使用。

该工具会为每个已分析页面生成一个报告,其中显示页面如何针对一组预定义的规则执行,并在测试结果超出基线值时显示详细信息。 SharePoint 管理员和设计人员可以使用该工具排查性能问题,以确保在发布前优化新页面。

特别是 SPRequestDuration 是 SharePoint 处理页面所需的时间。 繁重的导航 ((如在导航) 中包含页面、复杂的站点层次结构以及其他配置和拓扑选项)都可能会显著延长持续时间。

在 SharePoint 中使用结构化导航

这是默认使用的现用导航,是最直接的解决方案。 它不需要任何自定义,非技术用户也可以从设置页轻松添加项、隐藏项和管理导航。 建议 启用缓存,否则会进行昂贵的性能权衡。

如何实现结构化导航缓存

“网站设置外观>导航”>下,可以验证是否为全局导航或当前导航选择了结构导航。 选择 “显示页面 ”将对性能产生负面影响。

结构导航,其中选择了“显示子网站”。

可以在网站集级别和网站级别启用或禁用缓存,并且默认为这两者启用。 若要在网站集级别启用,请在“网站设置”“网站集管理>网站集导航”>下,检查“启用缓存”框。

在网站集级别启用缓存。

若要在网站级别启用,请在“网站设置导航”>下检查“启用缓存”框。

在站点级别启用缓存。

在 SharePoint 中使用托管导航和元数据

托管导航是另一个现成的选项,可用于重新创建与结构导航相同的大部分功能。 可以将托管元数据配置为启用或禁用安全修整。 如果配置禁用了安全修整,则托管导航相当高效,因为它加载具有固定数量的服务器调用的所有导航链接。 但是,启用安全修整会抵消托管导航的一些性能优势。

如果需要启用安全修整,建议:

  • 将所有友好 URL 链接更新为简单链接
  • 添加所需的安全修整节点作为友好 URL
  • 将导航项数限制为不超过 100 个,深度不超过 3 个级别

许多网站不需要安全修整,因为导航结构通常对于网站的所有用户都是一致的。 如果禁用了安全修整,并且向并非所有用户都有权访问的导航添加了链接,该链接仍将显示,但会导致拒绝访问的消息。 不存在无意访问内容的风险。

如何实现托管导航和结果

Microsoft 了解托管导航的详细信息有几篇文章。 有关示例,请参阅 SharePoint Server 中的托管导航概述

若要实现托管导航,请使用与网站的导航结构对应的 URL 设置术语。 在许多情况下,甚至可以手动策划托管导航以取代结构化导航。 例如:

SharePoint 网站结构。)

使用搜索驱动的客户端脚本

自定义导航实现的一个常见类包含客户端呈现的设计模式,这些模式存储导航节点的本地缓存。

这些导航提供程序具有以下几个主要优势:

  • 它们通常适用于响应式页面设计。
  • 它们具有极高的可缩放性和性能,因为它们可以在无资源成本 (呈现,并在超时) 后在后台刷新。
  • 这些导航提供程序可以使用各种策略检索导航数据,从简单的静态配置到各种动态数据提供程序。

数据提供程序的一个示例是使用搜索驱动的导航,这样可以灵活地枚举导航节点并有效地处理安全修整。

还有其他常用选项可用于生成 自定义导航提供程序。 有关构建自定义导航提供程序的进一步指导 ,请查看 SharePoint 门户 的导航解决方案。

使用搜索,可以使用连续爬网在后台构建的索引。 搜索结果从搜索索引中拉取,结果经过安全修整。 当需要安全修整时,这通常比现用的导航提供程序更快。 使用搜索结构导航(尤其是如果网站结构复杂)将大大加快页面加载时间。 与托管导航main优势在于,你可以从安全修整中受益。

此方法涉及创建自定义母版页,并将现成的导航代码替换为自定义 HTML。 按照以下示例中概述的过程替换 文件中 seattle.html的导航代码。 在此示例中,你将打开 文件, seattle.html 并将整个元素 id="DeltaTopNavigation" 替换为自定义 HTML 代码。

示例:替换母版页中的现成导航代码

  1. 导航到“网站设置”页。
  2. 单击“母版页”打开 母版页库。
  3. 在此处,可以浏览库并下载文件 seattle.master
  4. 使用文本编辑器编辑代码,并删除以下屏幕截图中的代码块。
    删除显示的代码块。
  5. 删除 和 <\SharePoint:AjaxDelta> 标记之间的<SharePoint:AjaxDelta id="DeltaTopNavigation">代码,并将其替换为以下代码片段:
<div id="loading">
  <!--Replace with path to loading image.-->
  <div style="background-image: url(''); height: 22px; width: 22px; ">
  </div>
</div>
<!-- Main Content-->
<div id="navContainer" style="display:none">
    <div data-bind="foreach: hierarchy" class="noindex ms-core-listMenu-horizontalBox">
        <a class="dynamic menu-item ms-core-listMenu-item ms-displayInline ms-navedit-linkNode" data-bind="attr: { href: item.Url, title: item.Title }">
            <span class="menu-item-text" data-bind="text: item.Title">
            </span>
        </a>
        <ul id="menu" data-bind="foreach: $data.children" style="padding-left:20px">
            <li class="static dynamic-children level1">
                <a class="static dynamic-children menu-item ms-core-listMenu-item ms-displayInline ms-navedit-linkNode" data-bind="attr: { href: item.Url, title: item.Title }">

                 <!-- ko if: children.length > 0-->
                    <span aria-haspopup="true" class="additional-background ms-navedit-flyoutArrow dynamic-children">
                        <span class="menu-item-text" data-bind="text: item.Title">
                        </span>
                    </span>
                <!-- /ko -->
                <!-- ko if: children.length == 0-->
                    <span aria-haspopup="true" class="ms-navedit-flyoutArrow dynamic-children">
                        <span class="menu-item-text" data-bind="text: item.Title">
                        </span>
                    </span>
                <!-- /ko -->
                </a>

                <!-- ko if: children.length > 0-->
                <ul id="menu"  data-bind="foreach: children;" class="dynamic  level2" >
                    <li class="dynamic level2">
                        <a class="dynamic menu-item ms-core-listMenu-item ms-displayInline  ms-navedit-linkNode" data-bind="attr: { href: item.Url, title: item.Title }">

          <!-- ko if: children.length > 0-->
          <span aria-haspopup="true" class="additional-background ms-navedit-flyoutArrow dynamic-children">
           <span class="menu-item-text" data-bind="text: item.Title">
           </span>
          </span>
           <!-- /ko -->
          <!-- ko if: children.length == 0-->
          <span aria-haspopup="true" class="ms-navedit-flyoutArrow dynamic-children">
           <span class="menu-item-text" data-bind="text: item.Title">
           </span>
          </span>
          <!-- /ko -->
                        </a>
          <!-- ko if: children.length > 0-->
         <ul id="menu" data-bind="foreach: children;" class="dynamic level3" >
          <li class="dynamic level3">
           <a class="dynamic menu-item ms-core-listMenu-item ms-displayInline ms-navedit-linkNode" data-bind="attr: { href: item.Url, title: item.Title }">
            <span class="menu-item-text" data-bind="text: item.Title">
            </span>
           </a>
          </li>
         </ul>
           <!-- /ko -->
                    </li>
                </ul>
                <!-- /ko -->
            </li>
        </ul>
    </div>
</div>

6. 将开头的正在加载图像定位标记中的 URL 替换为指向网站集中加载图像的链接。 进行更改后,重命名文件,然后将其上传到母版页库。 这会生成新的 .master 文件。
7. 此 HTML 是由 JavaScript 代码返回的搜索结果填充的基本标记。 需要编辑代码以更改 var root = “网站集 URL” 的值,如以下代码片段所示:
var root = "https://spperformance.sharepoint.com/sites/NavigationBySearch";

8.将结果分配给 self.nodes 数组,并使用 linq.js 将输出分配给数组 self.hierarchy 的对象构建层次结构。 此数组是绑定到 HTML 的对象。 这在 toggleView () 函数中通过将自对象传递给 ko.applyBinding () 函数来完成。
这会导致层次结构数组绑定到以下 HTML:
<div data-bind="foreach: hierarchy" class="noindex ms-core-listMenu-horizontalBox">

mouseexitmouseenter事件处理程序将添加到顶级导航中,以处理在 函数中addEventsToElements()完成的子网站下拉菜单。

在我们的复杂导航示例中,没有本地缓存的全新页面加载显示,在服务器上花费的时间已从基准结构导航中减少,以获得与托管导航方法类似的结果。

关于 JavaScript 文件...

注意

如果使用自定义 JavaScript,请确保已启用公共 CDN,并且文件位于 CDN 位置。

整个 JavaScript 文件如下所示:

//Models and Namespaces
var SPOCustom = SPOCustom || {};
SPOCustom.Models = SPOCustom.Models || {}
SPOCustom.Models.NavigationNode = function () {

    this.Url = ko.observable("");
    this.Title = ko.observable("");
    this.Parent = ko.observable("");

};

var root = "https://spperformance.sharepoint.com/sites/NavigationBySearch";
var baseUrl = root + "/_api/search/query?querytext=";
var query = baseUrl + "'contentClass=\"STS_Web\"+path:" + root + "'&trimduplicates=false&rowlimit=300";

var baseRequest = {
    url: "",
    type: ""
};


//Parses a local object from JSON search result.
function getNavigationFromDto(dto) {
    var item = new SPOCustom.Models.NavigationNode();
    if (dto != undefined) {

        var webTemplate = getSearchResultsValue(dto.Cells.results, 'WebTemplate');

        if (webTemplate != "APP") {
            item.Title(getSearchResultsValue(dto.Cells.results, 'Title')); //Key = Title
            item.Url(getSearchResultsValue(dto.Cells.results, 'Path')); //Key = Path
            item.Parent(getSearchResultsValue(dto.Cells.results, 'ParentLink')); //Key = ParentLink
        }

    }
    return item;
}

function getSearchResultsValue(results, key) {

    for (i = 0; i < results.length; i++) {
        if (results[i].Key == key) {
            return results[i].Value;
        }
    }
    return null;
}

//Parse a local object from the serialized cache.
function getNavigationFromCache(dto) {
    var item = new SPOCustom.Models.NavigationNode();

    if (dto != undefined) {

        item.Title(dto.Title);
        item.Url(dto.Url);
        item.Parent(dto.Parent);
    }

    return item;
}

/* create a new OData request for JSON response */
function getRequest(endpoint) {
    var request = baseRequest;
    request.type = "GET";
    request.url = endpoint;
    request.headers = { ACCEPT: "application/json;odata=verbose" };
    return request;
};

/* Navigation Module*/
function NavigationViewModel() {
    "use strict";
    var self = this;
    self.nodes = ko.observableArray([]);
    self.hierarchy = ko.observableArray([]);;
    self.loadNavigatioNodes = function () {
        //Check local storage for cached navigation datasource.
        var fromStorage = localStorage["nodesCache"];
        if (false) {
            var cachedNodes = JSON.parse(localStorage["nodesCache"]);

            if (cachedNodes && timeStamp) {
                //Check for cache expiration. Currently set to 3 hrs.
                var now = new Date();
                var diff = now.getTime() - timeStamp;
                if (Math.round(diff / (1000 * 60 * 60)) < 3) {

                    //return from cache.
                    var cacheResults = [];
                    $.each(cachedNodes, function (i, item) {
                        var nodeitem = getNavigationFromCache(item, true);
                        cacheResults.push(nodeitem);
                    });

                    self.buildHierarchy(cacheResults);
                    self.toggleView();
                    addEventsToElements();
                    return;
                }
            }
        }
        //No cache hit, REST call required.
        self.queryRemoteInterface();
    };

    //Executes a REST call and builds the navigation hierarchy.
    self.queryRemoteInterface = function () {
        var oDataRequest = getRequest(query);
        $.ajax(oDataRequest).done(function (data) {
            var results = [];
            $.each(data.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results, function (i, item) {

                if (i == 0) {
                    //Add root element.
                    var rootItem = new SPOCustom.Models.NavigationNode();
                    rootItem.Title("Root");
                    rootItem.Url(root);
                    rootItem.Parent(null);
                    results.push(rootItem);
                }
                var navItem = getNavigationFromDto(item);
                results.push(navItem);
            });
            //Add to local cache
            localStorage["nodesCache"] = ko.toJSON(results);

            localStorage["nodesCachedAt"] = new Date().getTime();
            self.nodes(results);
            if (self.nodes().length > 0) {
                var unsortedArray = self.nodes();
                var sortedArray = unsortedArray.sort(self.sortObjectsInArray);

                self.buildHierarchy(sortedArray);
                self.toggleView();
                addEventsToElements();
            }
        }).fail(function () {
            //Handle error here!!
            $("#loading").hide();
            $("#error").show();
        });
    };
    self.toggleView = function () {
        var navContainer = document.getElementById("navContainer");
        ko.applyBindings(self, navContainer);
        $("#loading").hide();
        $("#navContainer").show();

    };
    //Uses linq.js to build the navigation tree.
    self.buildHierarchy = function (enumerable) {
        self.hierarchy(Enumerable.From(enumerable).ByHierarchy(function (d) {
            return d.Parent() == null;
        }, function (parent, child) {
            if (parent.Url() == null || child.Parent() == null)
                return false;
            return parent.Url().toUpperCase() == child.Parent().toUpperCase();
        }).ToArray());

        self.sortChildren(self.hierarchy()[0]);
    };


    self.sortChildren = function (parent) {

        // sjip processing if no children
        if (!parent || !parent.children || parent.children.length === 0) {
            return;
        }

        parent.children = parent.children.sort(self.sortObjectsInArray2);

        for (var i = 0; i < parent.children.length; i++) {
            var elem = parent.children[i];

            if (elem.children && elem.children.length > 0) {
                self.sortChildren(elem);
            }
        }
    };

    // ByHierarchy method breaks the sorting in chrome and firefox
    // we need to resort  as ascending
    self.sortObjectsInArray2 = function (a, b) {
        if (a.item.Title() > b.item.Title())
            return 1;
        if (a.item.Title() < b.item.Title())
            return -1;
        return 0;
    };


    self.sortObjectsInArray = function (a, b) {
        if (a.Title() > b.Title())
            return -1;
        if (a.Title() < b.Title())
            return 1;
        return 0;
    }
}

//Loads the navigation on load and binds the event handlers for mouse interaction.
function InitCustomNav() {
    var viewModel = new NavigationViewModel();
    viewModel.loadNavigatioNodes();
}

function addEventsToElements() {
    //events.
      $("li.level1").mouseover(function () {
          var position = $(this).position();
          $(this).find("ul.level2").css({ width: 100, left: position.left + 10, top: 50 });
      })
   .mouseout(function () {
     $(this).find("ul.level2").css({  left: -99999, top: 0 });
   
    });
   
     $("li.level2").mouseover(function () {
          var position = $(this).position();
          console.log(JSON.stringify(position));
          $(this).find("ul.level3").css({ width: 100, left: position.left + 95, top:  position.top});
      })
   .mouseout(function () {
     $(this).find("ul.level3").css({  left: -99999, top: 0 });
    });
} _spBodyOnLoadFunctionNames.push("InitCustomNav");

为了总结函数中 jQuery $(document).ready 上面所示的代码,创建了 一个 viewModel object ,然后调用该 loadNavigationNodes() 对象上的函数。 此函数要么加载以前生成的导航层次结构存储在客户端浏览器的 HTML5 本地存储中,要么调用 函数 queryRemoteInterface()

QueryRemoteInterface() 使用 函数生成请求, getRequest() 并使用脚本前面定义的查询参数,然后从服务器返回数据。 此数据实质上是网站集中所有网站的数组,表示为具有各种属性的数据传输对象。

然后,将此数据分析为以前定义的 SPO.Models.NavigationNode 对象,这些对象使用 Knockout.js 创建可观测属性,以便数据将值绑定到我们之前定义的 HTML 中。

然后将对象放入结果数组中。 此数组使用 Knockout 解析为 JSON,并存储在本地浏览器存储中,以便在将来加载页面时提高性能。

此方法的好处

此方法的一个主要好处是,通过使用 HTML5 本地存储,导航在用户下次加载页面时会在本地存储。 使用搜索 API 进行结构导航,我们获得了重大性能改进;但是,执行和自定义此功能需要一些技术功能。

示例实现中,站点的排序方式与现成的结构导航相同;字母顺序。 如果想要偏离此顺序,则开发和维护会更加复杂。 此外,此方法要求你偏离支持的母版页。 如果未维护自定义母版页,您的网站将错过 Microsoft 对母版页所做的更新和改进。

上述代码具有以下依赖项:

当前版本的 LinqJS 不包含上述代码中使用的 ByHierarchy 方法,并且会中断导航代码。 若要解决此问题,请将以下方法添加到行 之前的 Flatten: function ()Linq.js 文件中。

ByHierarchy: function(firstLevel, connectBy, orderBy, ascending, parent) {
     ascending = ascending == undefined ? true : ascending;
     var orderMethod = ascending == true ? 'OrderBy' : 'OrderByDescending';
     var source = this;
     firstLevel = Utils.CreateLambda(firstLevel);
     connectBy = Utils.CreateLambda(connectBy);
     orderBy = Utils.CreateLambda(orderBy);

     //Initiate or increase level
     var level = parent === undefined ? 1 : parent.level + 1;

    return new Enumerable(function() {
         var enumerator;
         var index = 0;

        var createLevel = function() {
                 var obj = {
                     item: enumerator.Current(),
                     level : level
                 };
                 obj.children = Enumerable.From(source).ByHierarchy(firstLevel, connectBy, orderBy, ascending, obj);
                 if (orderBy !== undefined) {
                     obj.children = obj.children[orderMethod](function(d) {
                         return orderBy(d.item); //unwrap the actual item for sort to work
                     });
                 }
                 obj.children = obj.children.ToArray();
                 Enumerable.From(obj.children).ForEach(function(child) {
                     child.getParent = function() {
                         return obj;
                     };
                 });
                 return obj;
             };

        return new IEnumerator(

        function() {
             enumerator = source.GetEnumerator();
         }, function() {
             while (enumerator.MoveNext()) {
                 var returnArr;
                 if (!parent) {
                     if (firstLevel(enumerator.Current(), index++)) {
                         return this.Yield(createLevel());
                     }

                } else {
                     if (connectBy(parent.item, enumerator.Current(), index++)) {
                         return this.Yield(createLevel());
                     }
                 }
             }
             return false;
         }, function() {
             Utils.Dispose(enumerator);
         })
     });
 },

SharePoint Server 2013 中的托管导航概述

结构化导航缓存和性能