JavaScript-Muster und Leistung
Vor vielen Jahren erhielten wir mit ASP.NET das serverseitige Rendern von Steuerelementen in der Benutzeroberfläche, und das war gut. Für dieses serverseitige Rendern ist jedoch voll vertrauenswürdiger Code erforderlich. Da wir nun zu SharePoint und Office 365 übergegangen sind, ist voll vertrauenswürdiger Code keine Option mehr. Dies bedeutet, dass das serverseitige Rendern von Benutzeroberflächen-Steuerelementen für uns nicht mehr funktioniert.
Unternehmen benötigen trotzdem weiterhin benutzerdefinierte UI-Funktionen für ihre Websites und Apps. Dies bedeutet, dass benutzerdefinierte UI-Funktionen von der Serverseite zur Clientseite übergehen müssen.
Zum Rendern von Benutzeroberflächen-Steuerelementen ist nun clientseitiges JavaScript die richtige Wahl.
JavaScript-Muster
Wenn nun clientseitiges JavaScript der richtige Weg ist, was sind dann die besten Methoden zum Implementieren von clientseitigem JavaScript? Wo fängt man am besten an?
Es stehen mehrere Möglichkeiten zur Verfügung:
Option | Beschreibung |
---|---|
JavaScript-Einbettungen | Site.UserCustomActions oder Web.UserCustomActions ermöglichten die Einbeziehung des Scripts direkt in das Seitenmarkup. Dies wird im Ladeprogramm-Muster verwendet, das weiter unten erläutert wird. |
Vorlagen anzeigen | Gilt für Ansichten und Suche. Sie müssen keinen bestimmten, von einer App oder einem Anbieter gehosteten Code bereitstellen. Es ist einfach nur eine JavaScript-Datei, die (beispielsweise) in die Formatbibliothek hochgeladen werden kann, um Ansichten anzupassen. Sie können jede erforderliche Ansicht mit JavaScript erstellen. |
In SharePoint gehostete Add-Ins | Verwendet JSOM für die Kommunikation mit dem Hostweb oder dem Add-In-Web. Bietet Zugriff auf den Webproxy für domänenübergreifende Aufrufe |
Vom Anbieter gehostete Add-Ins | Ermöglicht die Erstellung von komplexen Anwendungen mit einer Vielzahl von Technologiestapeln, wobei gleichzeitig die sichere Integration in SharePoint beibehalten wird. |
JSLink | Ermöglicht Ihnen, eine oder mehrere JavaScript-Dateien in viele OOTB-Webparts und Ansichten zu laden. |
ScriptEditor-Webpart | Ziehen Sie das Script direkt mit ein, oder laden Sie es über Script-Tags mit Markup, um komplexe Einzelseitenanwendungen zu erstellen, die vollständig auf der SharePoint-Website gehostet werden. |
Denken Sie nicht, dass Sie auf diese Auswahlmöglichkeiten eingeschränkt sind, wenn Sie der Meinung sind, dass eine andere Option für Ihre Anforderungen bessert geeignet ist.
JavaScript-Leistung
Bei jedem Schritt der Entwicklung müssen Sie die Leistung im Auge behalten. Nachfolgend sind einige Dinge aufgeführt, die sich erheblich auf die Leistung von JavaScript auswirken können:
Option | Beschreibung |
---|---|
Verringern Sie die Anzahl von Anfragen. | Weniger Anforderungen bedeuten weniger Roundtrips zum Server, wodurch sich die Latenz reduziert. |
Rufen Sie nur die Daten ab, die Sie benötigten. | Verringern Sie die Datenmenge, die über das Netzwerk übertragen werden muss. Hierdurch wird außerdem die Serverauslastung verringert. |
Stellen Sie eine gute Erfahrung beim Laden von Seiten bereit. | Stellen Sie sicher, dass die Benutzeroberfläche jederzeit für den Benutzer funktioniert. Aktualisieren Sie zum Beispiel die Menüs auf der Seite, bevor Sie damit beginnen, 100 Datensätze und mehr herunterzuladen. |
Verwenden Sie asynchrone Aufrufe und Muster, wann immer möglich. | Polling wirkt sich nachteiliger auf die Leistung aus als die Verwendung eines asynchronen Aufrufs oder Rückrufs. |
Zwischenspeichern ist wichtig. | Das Zwischenspeichern reduziert die Auslastung des Servers noch weiter und führt zu einer enormen Leistungsverbesserung. |
Bereiten Sie sich auf mehr Seitenaufrufe vor, als Sie jemals für möglich gehalten haben. | Eine Startseite mit vielen Daten ist in Ordnung, wenn nur wenige Zugriffe erfolgen. Wenn jedoch Tausende von Benutzer die Seite besuchen, kann das die Leistung sehr beeinträchtigen. |
Was macht mein Code?
Was die Leistung angeht, ist es wichtig zu wissen, was Ihr Code zu jedem Zeitpunkt macht. Auf diese Weise können Sie ermitteln, wie Sie die Effizienz steigern können. Nachfolgend finden Sie einige gute Möglichkeiten, um genau das zu tun.
Verringern Sie die Anzahl von Anforderungen
Führen Sie immer möglichst wenige und kleine Anforderungen aus. Jede Anforderung, die Sie nicht senden, verringert die Leistungsbelastung des Servers und des Clients. Weniger umfangreiche Anforderungen verringern die Leistungsbelastung noch weiter.
Es gibt mehrere Möglichkeiten, die Anzahl von Anforderungen und die Größe von Anforderungen zu verringern.
- Schränken Sie die Anzahl von JavaScript-Dateien in der Produktion ein. Das Separieren von JavaScript-Dateien funktioniert bei der Entwicklung gut, bei der Produktion jedoch weniger gut. Kombinieren Sie JavaScript-Dateien in einer einzelnen JavaScript-Datei, oder in so wenigen JavaScript-Dateien wie möglich.
- Verringern Sie die Dateigrößen. Minimieren Sie die Dateigröße von JavaScript-Dateien in der Produktion, indem Sie Zeilenumbrüche, Leerräume und Kommentare entfernen. Es gibt zahlreiche Programme und Webseiten zum Minimieren der Größe von JavaScript-Dateien, die Sie nutzen können, um die JavaScript-Dateigrößen erheblich zu reduzieren.
- Verwenden die Zwischenspeicherung von Dateien im Browser, um die Anzahl von Anforderungen zu verringern. Das unten aufgeführte aktualisierte Ladeprogramm-Muster ist eine gute Möglichkeit, um diesen Ansatz weiter zu verfolgen.
Rufen Sie nur die Daten ab, die Sie benötigten
Denken Sie beim Anfordern von Daten daran, Ihre Anforderungen darauf zu fokussieren, was Sie tatsächlich benötigten. Wenn Sie zum Beispiel einen kompletten Artikel herunterladen, nur um den Titel zu erhalten, beeinträchtigt dies die Leistung doch ziemlich.
- Verwenden Sie Serverfilterung, -auswahl und -beschränkungen, um den Datenverkehr über das Netzwerk zu minimieren.
- Fordern Sie beispielsweise nicht alle Artikel an, wenn Sie nur die ersten fünf benötigen.
- Fordern Sie nicht den gesamten Eigenschaftenbehälter an, wenn Sie nur eine Eigenschaft benötigen. Bei einem Beispiel benötigte ein Skript nur eine Eigenschaft, fragte jedoch den gesamten Eigenschaftenbehälter ab, der 800 KB groß war. Beachten Sie außerdem, dass sich die Größe eines Objekts im Laufe der Zeit ändern kann. Das, was jetzt nur ein paar Kilobyte groß ist, kann später im Produktlebenszyklus mehrere Megabyte groß werden.
Fordern Sie keine Daten an, die Sie ungenutzt verwerfen
Wenn Sie mehr Daten abrufen, als Sie tatsächlich verwenden, betrachten Sie dies als Gelegenheit, die ursprüngliche Abfrage besser zu filtern.
- Fordern Sie nur die Felder an, die Sie benötigen, z. B. Name und Adresse, und nicht den gesamten Datensatz.
- Führen Sie spezifische, absichtliche Filteranforderungen aus. Wenn Sie zum Beispiel die verfügbaren Artikel auflisten möchten, rufen Sie den Titel, das Veröffentlichungsdatum und den Autor ab. Schließen Sie die restlichen Felder nicht in die Anforderung ein.
Stellen Sie eine gute Benutzeroberfläche bereit
Ruckelnde, inkonsistente Benutzeroberflächen beeinträchtigen nicht nur die tatsächliche Leistung, sondern auch die wahrgenommene Leistung. Schreiben Sie den Code so, dass eine reibungslose Benutzerumgebung gewährleistet ist.
- Verwenden Sie ein Drehfeld, um anzuzeigen, dass Elemente geladen werden oder ein Vorgang noch andauert.
- Seien Sie sich der Ausführungsreihenfolge des Codes bewusst, und gestalten Sie ihn so, dass eine optimale Benutzererfahrung gewährleistet ist. Wenn Sie beispielsweise viele Daten vom Server abrufen möchten und Sie die Benutzeroberfläche ändern möchten, indem Sie ein Menü ausblenden, blenden Sie das Menü zuerst aus. Hierdurch wird vermieden, dass die Benutzeroberfläche ruckelt.
Alles ist asynchron
Jede Codeaktivität im Browser sollte als asynchron betrachtet werden. Dateien werden in einer bestimmten Reihenfolge geladen, Sie müssen warten, bis das DOM geladen ist, und die Anforderungen an SharePoint dauern unterschiedlich lang.
- Seien Sie sich bewusst, wie Ihr Code im Zeitverlauf funktioniert.
- Verwenden Sie Ereignisse und Rückrufe statt Polling.
- Verwenden Sie Zusagen. In jQuery werden Sie als Deferred-Objekte bezeichnet. Es gibt ähnliche Konzepte in Q, WinJS und ES6.
- Verwenden Sie asynchrone Aufrufe anstatt synchrone Aufrufe.
- Verwenden Sie immer dann die asynchrone Möglichkeit, wenn es zu einer Verzögerung kommen könnte:
- Während eine AJAX-Anforderung.
- Während einer umfangreichen DOM-Bearbeitung.
Asynchrone Muster verbessern die Leistung und Reaktionszeiten und ermöglichen das effektive Verketten von abhängigen Aktionen.
Clientseitiges Zwischenspeichern
Das clientseitige Zwischenspeichern ist eine der Methoden zur Leistungsverbesserung, die am häufigsten übersehen wird, die Sie jedoch einfach zu Ihrem Code hinzufügen können.
Es gibt drei unterschiedliche Orte, an denen Sie Daten zwischenspeichern können:
Option | Beschreibung |
---|---|
Sitzungsspeicher | Speichert Daten als Schlüssel-Wert-Paar auf dem Client. Dies ist eine Speicherung pro Sitzung, die immer als Zeichenfolgen gespeichert wird. JSON.stringify() konvertiert Ihre JavaScript-Objekte in Zeichenfolgen, um Objekte zu speichern. |
Lokaler Speicher | Speichert Daten als Schlüssel-Wert-Paar auf dem Client. Dies ist eine dauerhafte Speicherung über Sitzungen hinweg, die immer als Zeichenfolgen gespeichert wird. JSON.stringify() konvertiert Ihre JavaScript-Objekte in Zeichenfolgen, um Objekte zu speichern. |
Lokale Datenbank | Speichert relationale Daten auf dem Client. Verwendet häufig SQL-Lite als Datenbankmodul. Lokaler Datenbankspeicher ist nicht immer in allen Browsern verfügbar– Überprüfen sie die Unterstützung des Zielbrowsers. |
Beim Zwischenspeichern müssen Sie die Speicherbegrenzungen beachten, die für Sie gelten, sowie die Aktualität der Daten.
- Wenn Sie die Speicherbegrenzungen erreichen, ist es ratsam, ältere oder weniger wichtige zwischengespeicherte Daten zu entfernen.
- Einige Arten von Daten können schneller altern als andere. Eine Liste mit Nachrichtenartikeln kann in fünf oder zehn Minuten veraltete sein, der Profilname eines Benutzers kann jedoch in der Regel bedenkenlos 24 Stunden oder länger im Zwischenspeicher gespeichert werden.
Der lokale und Sitzungsspeicher haben keinen integrierten Ablauf, Cookies jedoch schon. Sie können Ihre gespeicherten Daten mit einem Cookie verknüpfen, um dem lokalen und Sitzungsspeicher einen Ablauf hinzuzufügen. Sie können auch einen Speicher-Wrapper erstellen, der ein Ablaufdatum enthält, und dies im Code überprüfen.
Der Preis der Beliebtheit
Wie oft wird die Seite angezeigt? In einem herkömmlichen Szenario ist die Homepage des Unternehmens die Startseite in allen Browsern im Unternehmen. Plötzlich kommt wesentlich mehr Datenverkehr auf, als Sie jemals erwartet hatten. Jedes Inhaltsbyte wird in der Serverleistung und Bandbreite, die Ihre Startseite beansprucht, plötzlich vergrößert.
Die Lösung: Halten Sie die Homepage übersichtlich und klein, und verknüpfen Sie andere Inhalte über Links.
Dashboards mit umfangreichen Daten eignen sich ebenfalls für eine vom Anbieter gehostete App, die unabhängig skaliert werden kann.
Das Ladeprogramm-Muster
Ziel des Ladeprogramm-Musters ist es, eine Möglichkeit zu bieten, eine unbekannte Anzahl von Remote-Skripts in eine Website einzubetten, ohne die Website zu aktualisieren. Die Updates können auf dem Remote-CDN ausgeführt werden, und es werden dann alle Websites aktualisiert.
Das Ladeprogramm-Muster erstellt eine URL mit Datums- und Zeitstempel am Ende, sodass die Datei nicht zwischengespeichert wird. Es richtet jQuery als Abhängigkeit in der Ladeprogrammdatei ein und führt dann eine Funktion im Ladeprogramm aus. Dadurch wird sichergestellt, dass Ihr benutzerdefiniertes JavaScript geladen wird, nachdem das Laden der jQuery abgeschlossen ist.
PnP-dev\Samples\Core.JavaScript\Core.JavaScript.Embedder\Program.cs:
static void Main(string[] args)
{
ContextManager.WithContext((context) =>
// this is the script block that will be embedded into the page
// in practice this can be done during provisioning of the site/web
// make sure to include ';' at end to play nice with page embedding
// using the script on demand feature built into SharePoint we load jQuery, then our remote loader(pnp-loader.js or pnp-loader-cached.js) file using a dependency
var script = @"(function (loaderFile, nocache) {
var url = loaderFile + ((nocache) ? '?' + encodeURIComponent((new Date()).getTime()) : '');
SP.SOD.registerSod('pnp-jquery.js', 'https://localhost:44324/js/jquery.js');
SP.SOD.registerSod('pnp-loader.js', url);
SP.SOD.registerSodDep('pnp-loader.js', 'pnp-jquery.js');
SP.SOD.executeFunc('pnp-loader.js', null, function() {});
})('https://localhost:44324/pnp-loader.js', true);";
// this version of the script along with pnp-loaderMDS.js (or pnp-loaderMDS-cached.js) handles pages where the minimum download strategy is active
var script2 = @"ExecuteOrDelayUntilBodyLoaded(function () {
var url = 'https://localhost:44324/js/pnp-loaderMDS.js?' + encodeURIComponent((new Date()).getTime());
SP.SOD.registerSod('pnp-jquery.js', 'https://localhost:44324/js/jquery.js');
SP.SOD.registerSod('pnp-loader.js', url);
SP.SOD.registerSodDep('pnp-loader.js', 'pnp-jquery.js');
SP.SOD.executeFunc('pnp-loader.js', null, function () {
if (typeof pnpLoadFiles === 'undefined') {
RegisterModuleInit('https://localhost:44324/js/pnp-loaderMDS.js', pnpLoadFiles);
} else {
pnpLoadFiles();
}
});
});";
// load the collection of existing links
var links = context.Site.RootWeb.UserCustomActions;
context.Load(links, ls => ls.Include(l => l.Title));
context.ExecuteQueryRetry();
// this block handles deleting previous test custom actions
var doDelete = false;
foreach (var link in links.ToArray().Where(l => l.Title.Equals("MyTestCustomAction", StringComparison.OrdinalIgnoreCase)))
{
link.DeleteObject();
doDelete = true;
}
if (doDelete)
{
context.ExecuteQueryRetry();
}
// now we embed our script into the user custom action
var newLink = context.Site.RootWeb.UserCustomActions.Add();
newLink.Title = "MyTestCustomAction";
newLink.Description = "Doing some testing.";
newLink.ScriptBlock = script2;
newLink.Location = "ScriptLink";
newLink.Update();
context.ExecuteQueryRetry();
});
}
SP.SOD.registerSodDep('pnp-loader.js', 'pnp-jquery.js');
richtet eine Abhängigkeit ein, und SP.SOD.executeFunc('pnp-loader.js', null, function() {});
zwingt jQuery, vollständig zu laden, bevor das benutzerdefinierte JavaScript geladen wird.
newLink.ScriptBlock = script2;
und newLink.Location = "ScriptLink";
sind die wichtigsten Bestandteile, wenn dies zur Ihrer Benutzeraktion hinzugefügt wird.
Die Datei „pnp-loader.js“ lädt dann eine Liste von JavaScript-Dateien mit einer Zusage, die beim Laden der einzelnen Dateien ausgeführt werden kann.
PnP-dev\Samples\Core.JavaScript\Core.JavaScript.CDN\js\pnp-loader.js:
(function () {
var urlbase = 'https://localhost:44324';
var files = [
'/js/pnp-settings.js',
'/js/pnp-core.js',
'/js/pnp-clientcache.js',
'/js/pnp-config.js',
'/js/pnp-logging.js',
'/js/pnp-devdashboard.js',
'/js/pnp-uimods.js'
];
// create a promise
var promise = $.Deferred();
// this function will be used to recursively load all the files
var engine = function () {
// maintain context
var self = this;
// get the next file to load
var file = self.files.shift();
var fullPath = urlbase + file;
// load the remote script file
$.getScript(fullPath).done(function () {
if (self.files.length > 0) {
engine.call(self);
}
else {
self.promise.resolve();
}
}).fail(self.promise.reject);
};
// create our "this" we will apply to the engine function
var ctx = {
files: files,
promise: promise
};
// call the engine with our context
engine.call(ctx);
// give back the promise
return promise.promise();
})().done(function () {
/* all scripts are loaded and I could take actions here */
}).fail(function () {
/* something failed, take some action here if needed */
});
Die Datei „pnp-loader.js“ wird nicht zwischengespeichert, was in der Entwicklungsumgebung gut funktioniert. Die Datei „pnp-loader-cached.js“ ersetzt die $.getScript
-Funktion durch eine $.ajax
-Funktion, was das Zwischenspeichern von Dateien im Browser ermöglicht und sich für Produktionsumgebungen besser eignet.
Von PnP-dev\Samples\Core.JavaScript\Core.JavaScript.CDN\js\pnp-loader.js
// load the remote script file
$.ajax({
type: 'GET',
url: fullPath,
cache: true,
dataType: 'script'
}).done(function () {
if (self.files.length > 0) {
engine.call(self);
}
else {
self.promise.resolve();
}
}).fail(self.promise.reject);
Dieses Muster vereinfacht die Bereitstellung und Websiteaktualisierungen. Es ist besonders hilfreich beim Bereitstellen oder Aktualisieren über Tausende von Websitesammlungen hinweg.
Zwischenspeichern des aktuellen Benutzers
Wenn die Benutzerinformationen bereits zwischengespeichert sind, erhält diese Funktion die Daten aus dem Sitzungscache. Wenn die Benutzerinformationen nicht im Cache gespeichert ist, ruft sie die spezifischen Benutzerinformationen ab und speichert sie im Cache.
Hierbei wird ebenfalls „Deferred“ verwendet (die jQuery-Version einer Zusage). Wenn wir die Daten aus dem Cache oder vom Server erhalten, wird „Deferred“ aufgelöst. Wenn ein Fehler vorliegt, wird „Deferred“ abgelehnt.
Von PnP-dev\Samples\Core.JavaScript\Core.JavaScript.CDN\js\pnp-core.js:
getCurrentUserInfo: function (ctx) {
var self = this;
if (self._currentUserInfoPromise == null) {
self._currentUserInfoPromise = $.Deferred(function (def) {
var cachingTest = $pnp.session !== 'undefined' && $pnp.session.enabled;
// if we have the caching module loaded
if (cachingTest) {
var userInfo = $pnp.session.get(self._currentUserInfoCacheKey);
if (userInfo !== null) {
self._currentUserInfo = userInfo;
def.resolveWith(ctx || self._currentUserInfo, [self._currentUserInfo]);
return;
}
}
// send the request and allow caching
$.ajax({
method: 'GET',
url: '/_api/SP.UserProfiles.PeopleManager/GetMyProperties?$select=AccountName,DisplayName,Title',
headers: { "Accept": "application/json; odata=verbose" },
cache: true
}).done(function (response) {
// we also parse and add some custom properties as an example
self._currentUserInfo = $.extend(response.d,
{
ParsedLoginName: $pnp.core.getUserIdFromLogin(response.d.AccountName)
});
if (cachingTest) {
$pnp.session.add(self._currentUserInfoCacheKey, self._currentUserInfo);
}
def.resolveWith(ctx || self._currentUserInfo, [self._currentUserInfo]);
}).fail(function (jqXHR, textStatus, errorThrown) {
console.error('[PNP]=>[Fatal Error] Could not load current user data data from /_api/SP.UserProfiles.PeopleManager/GetMyProperties. status: ' + textStatus + ', error: ' + errorThrown);
def.rejectWith(ctx || null);
});
});
}
return this._currentUserInfoPromise.promise();
}
}
Muster für die Zwischenspeicherung unter Verwendung von „asynchron“ und „Deferred“
Ein weiteres Muster für die Zwischenspeicherung findet sich in pnp-clientcache.js storageTest, das von einem Modernisierer-storageTest übernommen wird. Es enthält Funktionen zum Hinzufügen, Entfernen, Abrufen und getOrAdd, die die zwischengespeicherten Daten zurückgeben, wenn sie sich im Cache befinden, oder es werden Daten vom Server abgerufen und dem Cache hinzugefügt, wenn sie sich nicht im Cache befinden. , die sich wiederholende Code schreiben, in die aufrufende Funktion speichert. Dadurch entfällt das Schreiben von sich wiederholendem Code in der aufrufenden Funktion. get verwendet JSON.parse zum Testen auf einen Ablauf, da der Ablauf keine Funktion im lokalen Speicher ist. _createPersistable speichert das JavaScript-Objekt im Cache des lokalen Speichers.
Von PnP-dev\Samples\Core.JavaScript\Core.JavaScript.CDN\js\pnp-clientcache.js:
// adds the client cache capability
caching: {
// determine if we have local storage once
enabled: storageTest(),
add: function (/*string*/ key, /*object*/ value, /*datetime*/ expiration) {
if (this.enabled) {
localStorage.setItem(key, this._createPersistable(value, expiration));
}
},
// gets an item from the cache, checking the expiration and removing the object if it is expired
get: function (/*string*/ key) {
if (!this.enabled) {
return null;
}
var o = localStorage.getItem(key);
if (o == null) {
return o;
}
var persistable = JSON.parse(o);
if (new Date(persistable.expiration) <= new Date()) {
this.remove(key);
o = null;
} else {
o = persistable.value;
}
return o;
},
// removes an item from local storage by key
remove: function (/*string*/ key) {
if (this.enabled) {
localStorage.removeItem(key);
}
},
// gets an item from the cache or adds it using the supplied getter function
getOrAdd: function (/*string*/ key, /*function*/ getter) {
if (!this.enabled) {
return getter();
}
if (!$.isFunction(getter)) {
throw 'Function expected for parameter "getter".';
}
var o = this.get(key);
if (o == null) {
o = getter();
this.add(key, o);
}
return o;
},
// creates the persisted object wrapper using the value and the expiration, setting the default expiration if none is applied
_createPersistable: function (/*object*/ o, /*datetime*/ expiration) {
if (typeof expiration === 'undefined') {
expiration = $pnp.core.dateAdd(new Date(), 'minute', $pnp.settings.localStorageDefaultTimeoutMinutes);
}
return JSON.stringify({
value: o,
expiration: expiration
});
}
},
Eine komplexere Verwendung von „asynchron“ und „Deferred“ finden Sie im Entwicklerdashboard in pnp-clientcache.js.
Ressourcen
Siehe auch
- Leistungsüberlegungen im SharePoint-Add-In-Modell
- Ausführen grundlegender Vorgänge unter Verwendung von JavaScript-Bibliothekscode in SharePoint 2013
- Codebeispiele für das clientseitige Rendering (JS-Link)
- JavaScript-Einbettung (Anpassen der SharePoint-Website-UI durch Verwendung von JavaScript)
- Durchsuchen von Anpassungen für SharePoint
- SharePoint-Add-In-Rezept – Benutzerdefinierter Feldtyp (durch Verwendung von clientseitigem Rendering)