Ответ 1
Существующие ответы были недостаточно подробными, поэтому позвольте мне предоставить подробный ответ с кодом. Я в основном следовал за предложением ответа JotaBe, и вот как именно.
Сначала я разработал схему для того, какой пользовательский ( "data" ) атрибут я использовал бы и создал вспомогательную функцию, чтобы применить ее таким образом, чтобы помочь мне быть совместимым с комплектом ASP.Net. Атрибут должен предоставить необходимую информацию для загрузки одного файла пакета при включении оптимизации объединения (BundleTable.EnableOptimizations = True
) и нескольких независимых файлов. Вы можете увидеть формат, который я определил для атрибута 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
Затем я могу применить этот атрибут на node, как это, чтобы указать, как он хочет, чтобы привязки нокаута применялись к себе и его потомкам и какие скрипты нужны, прежде чем это делать. Обратите внимание, как мое намерение состоит в том, чтобы иметь возможность ссылаться на один и тот же набор и модель из нескольких узлов без дублирования загрузки или наличия дубликатов экземпляров модели, если я специально не запрашиваю отдельные экземпляры модели с помощью forceNew
. Вероятно, было бы лучше добавить контейнер для размещения этого атрибута в одном месте, но я хочу показать, что это не нужно.
Views/Инвентарь/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
на этих конкретных узлах и только создать экземпляр модели представления один раз, если отдельные экземпляры модели явно не запрашиваются на нескольких узлах.
Сценарии/приложение/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 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 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 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, а также осуществлять загрузку по требованию и ненавязчивый механизм для определения сценариев и просматривать модели, связанные с привязками к нокауту.