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 门户 的导航解决方案。
使用搜索,可以使用连续爬网在后台构建的索引。 搜索结果从搜索索引中拉取,结果经过安全修整。 当需要安全修整时,这通常比现用的导航提供程序更快。 使用搜索结构导航(尤其是如果网站结构复杂)将大大加快页面加载时间。 与托管导航main优势在于,你可以从安全修整中受益。
此方法涉及创建自定义母版页,并将现成的导航代码替换为自定义 HTML。 按照以下示例中概述的过程替换 文件中 seattle.html
的导航代码。 在此示例中,你将打开 文件, seattle.html
并将整个元素 id="DeltaTopNavigation"
替换为自定义 HTML 代码。
示例:替换母版页中的现成导航代码
- 导航到“网站设置”页。
- 单击“母版页”打开 母版页库。
- 在此处,可以浏览库并下载文件
seattle.master
。 - 使用文本编辑器编辑代码,并删除以下屏幕截图中的代码块。
- 删除 和
<\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">
和 mouseexit
的mouseenter
事件处理程序将添加到顶级导航中,以处理在 函数中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 对母版页所做的更新和改进。
上述代码具有以下依赖项:
- Jquery- https://jquery.com/
- KnockoutJS - https://knockoutjs.com/
- Linq.js -
https://linqjs.codeplex.com/
或 github.com/neuecc/linq.js
当前版本的 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);
})
});
},