现有的答案不够详细,所以请允许我用代码提供详细的答案。我主要遵循 JotaBe 的回答的建议,这就是具体方法。
首先,我为我将使用的自定义(“数据”)属性设计了一个方案,并创建了一个辅助函数以帮助我与 ASP.Net 捆绑兼容的方式应用它。在打开捆绑优化 (BundleTable.EnableOptimizations = True) 时,该属性需要提供下载单个捆绑文件所需的信息,否则需要提供几个独立文件。您可以在下面代码的 cmets 中看到我为 data-model 属性确定的格式。这段代码进入了一个名为 Helpers.vbhtml 的文件,该文件被添加到我的主项目中的一个新文件夹 App_Code 中。
App_Code/Helpers.vbhtml
@*
Purpose: Retrieve a value for the WebUI-specific data-model attribute which will
apply knockout bindings for the current node based on the specified
bundle, factory, and context.
BundleNameUrl: Bundle URL like "~/bundles/inventory"
FactoryName: Client side factory class of the view model like "inventoryViewModel"
ContextName: Client side context object that provides methods for retrieving
and updating the data fromt he client, like "inventorycontext"
ForceNew: If True, a new instance of the view model will always be created;
If False, a previously created instance will be reused when possible.
Output: In debug mode, the escaped (") version of a string like
{"bundle": "~/bundles/inventory", "sources": ["/Scripts/app/inventory.datacontext.js",
"/Scripts/app/inventory.model.js","/Scripts/app/inventorydetail.viewmodel.js",
"/Scripts/app/inventory.viewmodel.js"], "factory": "inventoryViewModel",
"context": "inventorycontext", "forceNew": false}
Or in release mode, like
{"bundle": "~/bundles/inventory", "sources":
["/bundles/inventory?v=YaRZhEhGq-GkPEQDut6enckUI6FH663GEN4u2-0Lo1g1"],
"factory": "inventoryViewModel", "context": "inventorycontext", "forceNew": false}
*@
@Helper GetModel(BundleNameUrl As String, FactoryName As String, ContextName As String, Optional ForceNew As Boolean = False)
@Code
Dim result As New System.Text.StringBuilder()
result.Append("{""bundle"": """ & BundleNameUrl & """, ""sources"": [")
Dim httpCtx As New HttpContextWrapper(HttpContext.Current)
' When EnableOptimizations = True, there will be one script source URL per bundle
' When EnableOptimizations = False, each script in the bundle is delivered separately
If BundleTable.EnableOptimizations Then
result.Append("""" & System.Web.Mvc.UrlHelper.GenerateContentUrl( _
BundleResolver.Current.GetBundleUrl(BundleNameUrl), httpCtx) & """")
Else
Dim first As Boolean = True
For Each bundle In BundleResolver.Current.GetBundleContents(BundleNameUrl)
If first Then first = False Else result.Append(",")
result.Append("""" & System.Web.Mvc.UrlHelper.GenerateContentUrl(bundle, httpCtx) & """")
Next
End If
result.Append("], ""factory"": """ & FactoryName & """, ""context"": """ & ContextName & """")
result.Append(", ""forceNew"": " & If(ForceNew, "true", "false") & "}")
End Code
@<text>@result.ToString()</text>
End Helper
然后我可以在这样的节点上应用该属性,让它指示它希望如何将淘汰绑定应用于自身及其后代,以及在这样做之前需要哪些脚本。请注意,我的意图是如何能够从多个节点引用相同的脚本包和模型,而无需重复下载或具有重复的模型实例,除非我特别使用 forceNew 请求模型的单独实例。在一个地方添加一个容器来容纳这个属性可能会更好,但我想证明这不是必需的。
Views/Inventory/Details.html
<a href="#" data-bind="click: loadPrevious" data-model="@Helpers.GetModel("~/bundles/inventory", "inventoryDetailViewModel", "inventorycontext")" title="Previous">Previous</a>
<a href="#" data-bind="click: loadNext" data-model="@Helpers.GetModel("~/bundles/inventory", "inventoryDetailViewModel", "inventorycontext")" title="Next">Next</a>
<fieldset data-bind="with: fsItem" data-model="@Helpers.GetModel("~/bundles/inventory", "inventoryDetailViewModel", "inventorycontext")">
最后,我创建了一个在现有捆绑包中引用的 javascript 文件,该捆绑包始终被拉入_Layout.vbhtml。它具有处理新的“数据模型”属性所需的客户端代码。这个想法是在这些特定节点上调用ko.applyBindings,并且只实例化一次视图模型,除非在多个节点上明确请求模型的不同实例。
脚本/app/webui.main.js
// Make sure we have our namespace carved out, and we
// know we're going to put a scriptCache in it.
window.webui = window.webui || { "scriptCache": {} };
// Copied from http://stackoverflow.com/a/691661/78162
// jQuery's getScript uses a mechanism that is not debuggable
// when operating within the domain, so we use this code to
// make sure the code is always a debuggable part of the DOM.
window.webui.getScript = function (url, callback) {
var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
script.src = url;
// Handle Script loading
{
var done = false;
// Attach handlers for all browsers
script.onload = script.onreadystatechange = function () {
if (!done && (!this.readyState ||
this.readyState == "loaded" || this.readyState == "complete")) {
done = true;
if (callback)
callback();
// Handle memory leak in IE
script.onload = script.onreadystatechange = null;
}
};
}
head.appendChild(script);
// We handle everything using the script element injection
return undefined;
};
// Call knockout's applyBindings function based on values specified in the
// data-model attribute after the script is done downloading (which is the
// responsibility of the caller).
window.webui.applyBindings = function (cacheObj, forceNew, factory, context, node) {
// Store instantiated view model objects for each factory in
// window.webui.scriptCache[bundleName].models for reuse on other nodes.
cacheObj.models = cacheObj.models || {};
// If an instance of the model doesn't exist yet, create one by calling the
// factory function, which should be implemented in a script in the
// downloaded bundle somewhere. And the context object should have already
// been instantiated when the script was downloaded.
if (forceNew || !cacheObj.models[factory])
cacheObj.models[factory] = window.webui[factory](ko, window.webui[context]);
// Apply bindings only to the node where data-model attribute was applied
ko.applyBindings(cacheObj.models[factory], node);
};
// Callback function when a script specified in the data-model attribute is
// done being downloaded on demand.
window.webui.onModelLoaded = function (cacheObj) {
// Count how many scripts inteh bundle have finished downloading
cacheObj.loadedCount += 1;
// If we have downloaded all scripts in the bundle, call applyBindings
// for all the nodes stored in the onComplete array.
if (cacheObj.loadedCount == cacheObj.totalCount) {
for (var callback in cacheObj.onComplete) {
var onComplete = cacheObj.onComplete[callback];
window.webui.applyBindings(cacheObj, onComplete.forceNew,
onComplete.factory, onComplete.context, onComplete.node);
}
}
};
// Process the data-model attribute of one HTML node by downloading the related bundle
// scripts if they haven't yet been downloaded and then calling applyBindings based on
// the values embedded in the attribute.
window.webui.require = function (modelAttribute, node) {
model = $.parseJSON(modelAttribute);
// Keep a cache of all the bundles that have been downloaded so we don't download the same
// bundle more than once even if multiple nodes refer to it.
window.webui.scriptCache = window.webui.scriptCache || {};
// The cache is keyed by bundle name. All scripts in a bundle are downloaded before
// any bindings are applied.
if (!window.webui.scriptCache[model.bundle]) {
// Store the expectd count and the loaded count so we know when the last
// script in the bundle is done that it's time to apply the bindings.
var cacheObj = {
totalCount: model.sources.length, loadedCount: 0, onComplete:
[{ "factory": model.factory, "context": model.context, "node": node, "forceNew": model.forceNew }]
};
window.webui.scriptCache[model.bundle] = cacheObj;
// For each script in the bundle, start the download, and pass in cacheObj
// so the callback will know if it has downloaded the last script and what
// to do when it has.
for (var script in model.sources) {
window.webui.getScript(model.sources[script], function () {
window.webui.onModelLoaded(cacheObj)
});
}
} else {
// If the bundle referenced already has a space allocated in the cache, that means
// its scripts are already downloaded or are in the process of being downloaded.
var cacheObj = window.webui.scriptCache[model.bundle];
if (cacheObj.totalCount == cacheObj.loadedCount) {
// If the bundle is already completely downloadad, just apply the bindings directly
window.webui.applyBindings(cacheObj, model.forceNew, model.factory, model.context, node);
} else {
// If the bundle is still being downloaded, add work to be done when bindings
// are applied upon completion.
window.webui.scriptCache[model.bundle].onComplete.push({
"factory": model.factory, "context": model.context, "node": node, "forceNew": model.forceNew
});
}
}
};
// When the document is done loading, locate every node with a data-model attribute
// and process the attribute value with the require function above on that node.
$(document).ready(function () {
$('[data-model]').each(function () {
var model = $(this).data("model");
window.webui.require(model, this);
});
});
有了这个解决方案,我可以依靠现有的 ASP.NET MVC4 捆绑框架(我不需要 r.js)来优化和组合 javascript 文件,还可以实现按需下载和定义脚本的不显眼机制并查看与淘汰赛绑定相关的模型。