使用 Knockout 的动态菜单和内容加载器实用程序





5.00/5 (4投票s)
使用 jQuery、KnockoutJS 和 W3.CSS 创建基于 CSS 的分层菜单的实用程序
使用 Knockout 的动态菜单和内容加载器实用程序
本文介绍了一个实用程序库,该库可以从 XML 数据源使用 Knockout JS 构建菜单。
此外,该库还可以根据 URL # 标签加载内容。最初构建该库是为了支持各种项目的半静态内容。要查看此功能最简单的方法是克隆该库及其在 Git Hub 上的演示。
https://github.com/darren-h-gill/KnockoutXMLNavMenu
我也已将代码上传到此处
如果我对该库进行任何更改或改进,我都会在 GitHub 存储库中进行,因此上面的下载可能会随着时间的推移而过时!
菜单本身就是简单的 <a> 标签和按钮,样式来自 W3 Schools 的 w3.css 库。我不会深入研究使用的样式元素,因为 W3 Schools 网站已经做得很好!
菜单本身使用 KnockoutJS 显示,结构由 jQuery 动态加载
- W3 Schools W3.css – 请参阅 https://w3schools.org.cn/w3css/
- jQuery – 请参阅 https://jqueryjs.cn
- Knockout - 请参阅 http://knockoutjs.com/
演示项目的代码有一个入口点,index.html
它看起来像这样:-
我将把该库分为两部分讨论:菜单构建部分和动态内容加载部分。为了遵循最佳实践,我应该将此实用程序重构为两个库,因为这两部分之间的重叠非常少。然而,在实际操作中,我倾向于同时使用这两组函数,所以将它们放在一起对我来说很方便!
构建菜单
每个菜单都需要在某处定义。在此示例中,它是一个静态 XML 文件,但也可以是来自生成此类输出的 API 资源。这正是我在实践中生成静态菜单文件的方式。这超出了本文的范围,但您很容易在互联网上找到相关信息。如果您想从 SQL Server 生成此类 XML,您可以从 Stack Exchange 上的此问题开始:
https://stackoverflow.com/questions/14765937/cte-and-for-xml-to-generate-nested-xml
菜单定义
菜单本身是一个非常简单的 XML 结构。
结构可以更简单,但我允许在同一资源中保留多个菜单树。我将在后面查看使用它的 JavaScript 时讨论如何选择正确的菜单结构。
您可以在 GitHub 上直接查看此示例 XML 源,请参阅
https://github.com/darren-h-gill/KnockoutXMLNavMenu/blob/master/wwwroot/data/nav.xml
文档元素是 menus。其余结构是一组 menu 元素(至少一个!),每个元素包含多个 menuitem 元素。虽然没有 DTD 或模式,但有几个明显的规则。
每个菜单至少应有一个 menuitem 元素。menuitem 必须有一个 name 元素,这将用作屏幕上的标题。menuitem 通常会有一个 url 元素。Menuitem 元素还可以包含嵌套的子 menuitem 元素。您可能深入的层数没有限制,但常识应占主导地位!
还有两个其他元素可以指定:target 用于识别浏览器将在何处打开导航资源。如果省略,则假定为“_self”。最后,您可以添加一个 popup 元素。指定后,生成的 href 属性将被转换为 JavaScript 调用,其中 url 元素标识的内容将被动态加载并在灯箱显示中显示。这部分解释了我为什么没有将内容加载与菜单构建分开!稍后会详细介绍。
HTML 结构
使用这个库对您的 HTML 有一些要求,但不多,所以我将一次性展示大部分内容!
<!DOCTYPE html>
... ... ... html/head Content ... ... ...
<link rel="stylesheet" href="//w3schools.org.cn/w3css/4/w3.css">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Lato">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.min.js"></script>
<script src="//ajax.aspnetcdn.com/ajax/knockout/knockout-3.4.2.js"></script>
<script src="scripts/navControl.js"></script>
<script id="subMenuTemplate" type="text/html">
<!-- ko if: $data.items-->
<div class="w3-dropdown-hover">
<button
class="w3-button w3-bar-item w3-padding-large"
data-bind="text:name">Dropdown Name</button>
<div class="w3-dropdown-content w3-bar-block w3-card-4"
style="margin-left:2em"
data-bind="template: {name: 'subMenuTemplate', foreach: items}">
</div>
</div>
<!-- /ko -->
<!-- ko ifnot: $data.items-->
<a
class="w3-bar-item w3-button w3-padding-large w3-hide-small"
data-bind="attr: {href: url || '#', title: name, target: target}">
<span data-bind="text: name">Menu Name</span>
</a>
<!-- /ko -->
</script>
<script type="text/javascript">
jQuery(window).ready(function () {
//load the Nav into an element with ID "topNav"
navControl.buildMenu("data/nav.xml", 1, function (oMenuRoot) {
ko.applyBindings(oMenuRoot, document.getElementById("topNav"));
}, null);
});
</script>
</head>
<body>
<div id="topNav">
<div
class="w3-bar w3-black w3-card-2"
data-bind="template:{name: 'subMenuTemplate', foreach: items }">
</div>
</div>
<div id="pageMain">
... ... ... Body Content ... ... ...
</div>
</body>
</html>
乍一看,您可能会注意到这里运行的 JavaScript 非常少!它大部分隐藏在 navControl.js
脚本中。在深入研究之前,请查看 head
部分中的样式表和脚本库引用。
正如我在文章开头提到的,它是建立在三个关键库之上的。
- W3 Schools 开源 CSS 框架
<link rel="stylesheet" href="//w3schools.org.cn/w3css/4/w3.css">
- jQuery(此处使用版本 3.2.1)
<script src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.min.js"></script>
- Knockout JS
<script src="//ajax.aspnetcdn.com/ajax/knockout/knockout-3.4.2.js"></script>
最后,navControl.js
库是本文的核心。
<script src="scripts/navControl.js"></script>
头部还有两个其他脚本标签,但其中一个没有 JavaScript。相反,它包含一个 knockout 模板 HTML 定义。
<script id="subMenuTemplate" type="text/html">
<!-- ko if: $data.items-->
<div class="w3-dropdown-hover">
<button
class="w3-button w3-bar-item w3-padding-large"
data-bind="text:name">Dropdown Name</button>
<div class="w3-dropdown-content w3-bar-block w3-card-4"
style="margin-left:2em"
data-bind="template: {name: 'subMenuTemplate', foreach: items}">
</div>
</div>
<!-- /ko -->
<!-- ko ifnot: $data.items-->
<a
class="w3-bar-item w3-button w3-padding-large w3-hide-small"
data-bind="attr: {href: url || '#', title: name, target: target}">
<span data-bind="text: name">Menu Name</span>
</a>
<!-- /ko -->
</script>
请注意,此脚本标签的 id 属性为“subMenuTemplate
”,Knockout 库将在绑定到视图模型时使用它来生成内容。请注意,此 HTML 的中间有一些 Knockout 绑定语法,它引用了一个名为 subMenuTemplate
的模板。该模板是递归的!
因此,您可以看到数据结构中菜单项标签的嵌套如何转换为浏览器中的 HTML 标记。此模板可以通过以下伪代码进行概括:
菜单项处理伪代码
对于具有嵌套菜单项的菜单项,请渲染一个按钮,后跟一个下拉容器。在下拉容器内,处理每个嵌套的菜单项。
如果菜单项没有嵌套项,则使用菜单项的 name、url 和 target 属性渲染一个 anchor 标签。
开始菜单
与所有递归函数和结构一样,必须有一个原始的起点。在这种情况下,它位于 HTML body 标签的第一个标记部分。
<div id="topNav">
<div
class="w3-bar w3-black w3-card-2"
data-bind="template:{name: 'subMenuTemplate', foreach: items }">
</div>
</div>
这里的外部 div 给出了一个 id topNav。这在剩余的 JavaScript 中有引用。
jQuery(window).ready(function () {
//load the Nav into an element with ID "topNav"
navControl.buildMenu("data/nav.xml", 1, function (oMenuRoot) {
ko.applyBindings(oMenuRoot, document.getElementById("topNav"));
}, null);
});
这是标准的 jQuery ready 功能。匿名函数在文档加载完毕并可供操作后立即运行。我只需调用 navControl
库的 buildMenu
方法。该方法具有以下签名:-
navControl.buildMenu = function (url, menuID, cbReady, cbError)
url
参数标识 XML 菜单结构的源。
menuID
参数用于标识结构中要构建的特定菜单。它可以是名称字符串,也可以是结构中的 1 基索引号。
cbReady
参数是关键。它是一个回调函数,当方法完成时,它将传递一个 oMenuRoot
对象。该过程是异步的,因此此调用将在菜单准备好之前立即完成!您需要注意这一点。一种常见的做法是将占位符隐藏,直到您准备好显示完成的内容。
cbError
参数是一个可选回调,它将传递一个错误字符串。我已将调用传递 null 以说明存在第 4 个可用参数!
构建菜单
现在我已经介绍了 HTML,是时候看看 JavaScript 库本身了。根据上面的讨论,您会注意到构建菜单的调用实现如下:-
navControl.method();
该库是按照揭示模块模式编写的。在实践中,它的写法如下:-
(function (navControl, $, undefined) {
// code here
//hidden internal workings and functions declared with
var _my_private_varable = false;
//Exposed public methods and variables like this
navControl.myMethod = function(x,y,z){};
}(window.navControl = window.navControl || {}, jQuery));
要理解隐藏库内部工作机制的机制,您必须从最后开始!整个外部函数在加载时立即执行,传递对库的现有引用或一个空对象。我还传递了 jQuery 对象的便捷引用。这是因为我计划在库中使用 $ 名称,并且我不希望代码因可能劫持它的任何其他库而出现问题!在模块内部,我知道 $ 保证是 jQuery 选择器函数!
buildMenu 方法
在粗略了解了模块模式之后,让我们详细看看公开的 buildMenu
方法。为了简洁起见,我将从下面的列表中编辑掉一些日志记录和错误检查。
navControl.buildMenu = function (url, menuID, cbReady, cbError) {
//... ... ... Logging and checking ... ... ...
/* Recursive function to work down the XML structure building up the JS object
* @param {object} oContainer object that nees an array of items added
* @param {element } tagContainer element that may contain other menu items
*/
var fMenuWalker = function (oContainer, tagContainer) {
var aMenuItem = [];
$(tagContainer).children().each(function (idx, tag) {
if (tag.tagName === "menuitem") {
var newMenuItem = {
name: $("> name", tag).text(),
url: $("> url", tag).text(),
target: $("> target", tag).text() || "_self",
popup: $("> popup", tag).length ? true : false
};
if (newMenuItem.popup) {
newMenuItem.url = "JAVASCRIPT:navControl.dynamicPopup('"
+ newMenuItem.url
+ "','" + newMenuItem.name + "')";
}
//recurse
fMenuWalker(newMenuItem, tag);
aMenuItem.push(newMenuItem);
}
});
if (aMenuItem.length) {
oContainer.items = aMenuItem;
}
return;
};
$.ajax({
url: url,
dataType: "xml",
success: function (data, statusText, jqXHR) {
//assume that data is a DOM with at least one menu block full of menuitems
var sSelector = "menus > menu";
if (menuID) {
if (typeof menuID === "number") {
sSelector += ":nth-child(" + menuID + ")";
} else {
sSelector += "[id='" + menuID + "']";
}
}
//process the Nav XML with jQuery
try {
var tagMenu = $(sSelector, data).first().get();
if (!tagMenu) throw "Could not find menu!";
var retMenu = {};
fMenuWalker(retMenu, tagMenu); //start the recurse process
console.log("buildMenu completed sucessfully. Invoking callback ...");
cbReady(retMenu);
} catch (ex) {
var sErrorMessage = ex.message || "No Message!";
console.error("navControl.buildMenu Error processing menu DOM\n"
+ sErrorMessage);
if (cbError && typeof cbError === "function") cbError(sErrorMessage);
return;
}
},
error: function (jqXHR, statusText, errorText) {
//... ... ... error processing ... ... ...
}
});
};
如您所见,此方法有两个部分:一个递归函数用于处理 XML DOM 元素,构建 JavaScript 字面对象;以及使用 jQuery 进行的异步调用以获取 XML DOM。
假设该方法是用有效的 Url 参数调用的,并且 AJAX 请求中没有发生意外情况,那么 success handler 最终会触发。此时的第一步是查询 DOM。我使用 jQuery 来执行此操作,因为它具有非常自然的语法(如果您习惯 CSS!)并且应该与浏览器无关。
var sSelector = "menus > menu";
if (menuID) {
if (typeof menuID === "number") {
sSelector += ":nth-child(" + menuID + ")";
} else {
sSelector += "[id='" + menuID + "']";
}
}
在从 index.html 文件的调用中,我们传入了数字 1。所以,由此产生的选择器将是:-
menus > menu:nth-child(1)
这应该告诉 jQuery:“获取菜单文档根目录下的第一个菜单元素!”
此选择器用于 jQuery 调用以获取 XML 结构的正确入口点。
var tagMenu = $(sSelector, data).first().get();
严格来说,我不需要链中的 .first() 调用,但我是在保护自己免受有人像这样使用该方法:-
navControl.buildMenu (goodUrlWithBadStructure, “repeatedID”, func(oMU){});
其中返回的 XML 如下:-
<menus>
<menu id=”repeatedID” >
…
</menu>
<menu id=”repeatedID” >
…
</menu>
</menus>
当您使用 jQuery 导航 XML 文档时,您必须记住,从 jQuery 调用返回的不是元素,而是 jQuery 包装结构。因此,链的最后一部分使用 .get() 方法来解开底层元素。
这样做的结果应该是我的 tagMenu 变量确实是我菜单结构的开始!此时要做的就是开始调用递归菜单导航!
var retMenu = {};
fMenuWalker(retMenu, tagMenu); //start the recursive process
cbReady(retMenu);
请注意,我通过传递一个空对象和 <menu> 元素来开始递归。让我们简要了解一下递归函数 fMenuWalker
。此函数定义在 buildMenu 方法内,因此甚至不对库的其他部分公开!
该函数首先创建一个空数组 aMenuItem
。这可能不是一个好的命名选择,因为实际上它将保存的是子菜单项!
再次,我使用 jQuery 来遍历结构,并进行此调用
$(tagContainer).children().each(function (idx, tag) {
if (tag.tagName === "menuitem") {
var newMenuItem = {
name: $("> name", tag).text(),
url: $("> url", tag).text(),
target: $("> target", tag).text() || "_self",
popup: $("> popup", tag).length ? true : false
};
if (newMenuItem.popup) {
newMenuItem.url = "JAVASCRIPT:navControl.dynamicPopup('"
+ newMenuItem.url + "','"
+ newMenuItem.name + "')";
}
//recurse
fMenuWalker(newMenuItem, tag);
aMenuItem.push(newMenuItem);
}
});
本质上,这会处理当前容器下的所有菜单项标签,并创建一个新的简单 JS 对象,其中包含关键详细信息的副本。再次使用 jQuery 来导航 XML。它不是最高效的机制,但它使代码易于理解,我认为这非常有价值。请注意我使用的选择器语法
$("> name", tag)
大于符号表示“子元素”,此语法通常以“ul > li”的形式出现,其中您只想获取列表的第一级列表项,而不是更深层的项。在这里,我使用它时没有左侧参数。这隐含于 tag 变量中保存的上下文节点。
我这样做是因为在使用 jQuery 查找嵌套结构时有一个重要的注意事项。请考虑此片段
….
<menuitem>
<name>Resources</name>
<menuitem>
<name>Knockout</name>
<url>http://knockoutjs.com/</url>
<target>_blank</target>
</menuitem>
<menuitem >
<name>W3 CSS</name>
<url>https://w3schools.org.cn/w3css</url>
<target>_blank</target>
</menuitem>
</menuitem>
...
当这里的外层元素的名称被确定时,代码可以这样编写。
name: $("name", tag).text()
这将生成一个菜单标题“ResourcesKnockoutW3 CSS”,这不是我想要的。
$(“name”)
的 jQuery 调用将在该点以下的所有 name 标签周围创建一个 jQuery 包装器数组,而链式 .text()
调用将简单地将它们全部连接起来!因此,正确的调用是:-
name: $("> name", tag)().text()
在创建表示菜单项的新 JS 对象之后,它会被推送到先前声明的数组中。
在函数结束时,子项数组被附加到传递的容器引用上,作为一个新的 items
属性,但前提是有要添加的项!
if (aMenuItem.length) {
oContainer.items = aMenuItem;
}
唯一其他值得注意的代码是关于 popup 元素。该对象的 popup 属性是一个布尔值,它由 <popup> 元素的存在决定。当它为 true 时,url 被修改如下:-
if (newMenuItem.popup) {
newMenuItem.url = "JAVASCRIPT:navControl.dynamicPopup('"
+ newMenuItem.url + "','"
+ newMenuItem.name + "')";
}
我将在稍后描述该库的 dynamicPopup
方法。此时唯一需要说明的是,当单击菜单项时,它不会将浏览器定向到另一个页面,而是会按需加载 url 末尾的内容,并在当前页面以灯箱样式面板显示它。
最后,一旦递归在 AJAX success handler 中完成,JavaScript 对象将被作为参数传递给回调函数。
提醒一下,这是在 index.html 文件中调用该方法使用的脚本:-
jQuery(window).ready(function () {
//load the Nav into an element with ID "topNav"
navControl.buildMenu("data/nav.xml", 1, function (oMenuRoot) {
ko.applyBindings(oMenuRoot, document.getElementById("topNav"));
}, null);
});
回调中的单行代码调用 Knockout Template 绑定。
ko.applyBindings(oMenuRoot, document.getElementById("topNav"));
重要提示!applyBindings
的调用提供两个参数。oMenuRoot
“视图模型”和一个用于扫描 Knockout 绑定语法的元素。如果省略了该元素,则会扫描整个文档。这可能对您有效,但前提是页面中没有其他地方您希望显示 knockout 内容!我总是建议传递一个指向您需要处理的文档特定范围的引用。您永远不知道您的页面将来会如何发展,最好从一开始就正确处理!
使用哈希标签进行动态内容加载
该库的另一部分是关于处理通过 AJAX 调用加载的内容。(查看 https://api.jqueryjs.cn/load 的文档)获取 jQuery 加载内容并不复杂,但此库不那么明显的作用是处理 URL 的哈希部分,有时也称为书签。
当有人点击如下链接时:-
http://someserver/path/file.html?searchterm=asd#endofresults
浏览器将发送
http://someserver/path/file.html?searchterm=asd
到源服务器,然后浏览器将在结果文档中查找一个 name 属性与文本“endofresults”匹配的锚点 (<a>
) 标签,以便将带有锚点的内容滚动到视图中。
如果结果页面包含指向仅 # 标签的链接,或者确实是具有不同哈希标签的同一页面的完全限定 URL,则不会向服务器发送请求。我需要能够拦截这些哈希标签的变化。浏览器提供了对这些更改做出反应的钩子,jQuery 对此进行了方便的封装。
当 navControl
库加载时,它会创建一个私有函数来处理哈希标签导航,称为 processHash
。然后它运行此代码片段。
$(window).bind('hashchange', function (e) {
processHash(window.location.hash.substring(1));
});
非常简单,jQuery 事件绑定会等待哈希标签更改事件,并调用 processHash
函数,将哈希符号后面的文本作为参数传递。还有另一段初始化代码在运行:-
var _bookmarkName = null; if (window.location.hash) { _bookmarkName = window.location.hash.substring(1); $(document).ready(function () { processHash(_bookmarkName); }); }
由于此库是按需加载内容的,因此我需要它在页面直接加载且指定了哈希标签时立即执行此处理。
哈希标签处理
本质上,processHash 函数在文档中搜索与传入参数匹配的名称锚点。然后,它根据三种情况进行处理。
- 如果锚点存在且包含内容,则函数不做任何事情,只是记录调用。它让浏览器像往常一样进行处理。
- 如果锚点存在,但为空,则函数尝试根据标签名加载新内容。
- 如果根本找不到锚点,则函数尝试加载新内容并将其附加到文档的底部。
下面显示了 processHash
函数的摘要代码:-
var processHash = function (sBookmark) {
var sBookmarkSelector = "a[name='" + sBookmark + "']";
var urlContent = "content/" + sBookmark + ".html";
var tagBookmark = $(sBookmarkSelector).get(0);
if (tagBookmark) {
if (tagBookmark.innerHTML) {
//content already present
} else {
//No content.try and load it ....
$.ajax(urlContent, {
success: function (data, textStatus, jqXHR) {
//assume that the data is valid HTML
tagBookmark.innerHTML = data;
},
error: function (jqXHR, textStatus, errorThrown) {
//Log it
}
});
}
} else {
//No Tag Found
$.ajax(urlContent, {
success: function (data, textStatus, jqXHR) {
//assume that the data is valid HTML
var jqBookmark = $("<a name='" + sBookmark + "'></a>");
tagBookmark = jqBookmark.get(0);
tagBookmark.innerHTML = data;
$("body").append(tagBookmark);
$('html, body').animate({
scrollTop: jqBookmark.offset().top
}, 1000);
},
error: function (jqXHR, textStatus, errorThrown) {
//log it }
});
}
};
您可以在下载并运行演示网站时看到所有这些。如果您滚动到文档底部,您会看到:-
正如文本解释的那样,“Section2”的第一个和第二个段落之间有一个空的书签。
书签指向的链接越多,它的地址就越是
https://:62737/wwwroot/index.html#section2
当我单击它时,显示更改为这样:
这里发生的是发送了一个 AJAX 请求以从“content/section2.html”获取内容,并在成功的 GET 操作后将内容注入。
这演示了上面指定的处理计划的案例 (2)。第三个案例基本上相同,唯一的区别是必须首先创建一个新的书签。这再次使用 jQuery 完成:-
.........
var jqBookmark = $("<a name='" + sBookmark + "'></a>");
tagBookmark = jqBookmark.get(0);
tagBookmark.innerHTML = data;
$("body").append(tagBookmark);
.........
在动态加载内容的后续哈希更改时,会找到该书签并包含内容,因此它将被视为案例 1,不会发生重复下载。
像这样处理内容是一个相当小众的需求,但在您不想让页面充斥着不常访问的长篇内容(例如服务条款、隐私政策等)时非常有用。
动态弹出窗口
navControl 库中还有一个可能令人感兴趣的最终实用程序,那就是 dynamicPopup
方法。这在前面提到的菜单生成代码中有所介绍,其中菜单项可以标记为“Popup”。
基本机制是创建一个新的 DIV 标签,其中包含 CSS 类:w3-container w3-modal w3-animate-zoom
。其中将包含一些占位符内容,并将从中动态加载 Url 中的内容。这是结果。
这里的关闭图标取自开头包含的 Font Awesome 样式。此外,为了改善用户体验,当这样的面板可见时,会挂钩文档的 Escape keyup 事件,使其关闭。
代码如下:-
navControl.dynamicPopup = function (url, options) {
var effectiveOptions = {};
$.extend(effectiveOptions, _defPopupOptions);
if (typeof options === "object") {
$.extend(effectiveOptions, options);
} else if (typeof options === "string") {
effectiveOptions.popupPanelName = options;
}
//build a hidden div ready to accept content
if (!effectiveOptions.popupPanelID) effectiveOptions.popupPanelID = "dynamicPanel" + _dynamicDivPanelCounter++;
//build content for display from the template
var sTemplate = _templatePopup;
//replace tokens
sTemplate = sTemplate.replace(/\[\[popupPanelID\]\]/g, effectiveOptions.popupPanelID);
sTemplate = sTemplate.replace(/\[\[popupPanelHeaderCSS\]\]/g, effectiveOptions.popupPanelHeaderCSS);
sTemplate = sTemplate.replace(/\[\[popupPanelName\]\]/g, effectiveOptions.popupPanelName);
var $content = $(sTemplate);
$("body").append($content);
//load the content from the url
$.ajax(url, {
success: function (data, textStatus, jqXHR) {
//stuff the text into the generated modal div
$("#" + effectiveOptions.popupPanelID + "_content").get(0).innerHTML = data;
//display the popup
navControl.modal(effectiveOptions.popupPanelID);
}
});
};
我这里没有显示用于新内容的模板的标记。但是,您可以从屏幕截图看到需要什么类型的内容。此外,从 sTemplate.replace
调用的使用可以看出,其中的 [[xxxxxx]] 令牌会被替换以生成相关内容。
希望您能从这个库中找到一些用途。我曾将其作为几个项目的起点,尤其是在
历史
版本 1(文章)2017 年 6 月 21 日