Über unsLeistungenProjekteR&DBlogWerkzeugeAnfangenKontakt

Entwickeln für SharePoint Server Subscription Edition: die Praktiken, die halten

Subscription Edition wird mindestens bis 2035 unterstützt, und ihre SharePoint-Framework-Obergrenze hat sich seit 2023 nicht bewegt. Diese Kombination ist die ganze Aufgabe: eine lange Bahn auf einem stabilen Unterbau. Wie man darauf gut baut, mit Code, der auf der Version läuft, die Ihre Serverfarm tatsächlich akzeptiert.

pH7x Systems® · · 22 Min. Lesezeit

Subscription Edition wird mindestens bis zum 31. Dezember 2035 unterstützt, unter der Modern Lifecycle Policy, ohne geplantes Supportende. Das ist eine längere Bahn als die der meisten Software, die Sie dieses Jahr schreiben werden.

Ihre SharePoint-Framework-Obergrenze hat sich dagegen seit 2023 nicht bewegt.

Diese beiden Tatsachen zusammen sind die ganze Aufgabe. Sie bauen etwas, das ein Jahrzehnt lang weiterlaufen muss, auf einer Framework-Version, die sich unter Ihnen vermutlich nicht ändern wird. Das ist eine ungewöhnliche Lage, und sie belohnt eine ganz bestimmte Sorgfalt.

Die Obergrenze kennen, bevor man irgendetwas schreibt

SharePoint on-premises führt nur die SPFx-Versionen aus, die zu seinen serverseitigen Abhängigkeiten passen. Die Kompatibilitätstabelle nennt für Subscription Edition v1.0 bis v1.5, und das Funktionsupdate 23H1 fügte Unterstützung für SPFx 1.5.1 hinzu, damals beschrieben als "ein Schritt auf unserem langfristigen Weg, die Fähigkeiten von SharePoint Framework in SharePoint Server Subscription Edition zu verbessern und auszuweiten".

Das nächste Update, 23H2, ging einen Schritt weiter und fügte Unterstützung für React 16 und Office UI Fabric React 7 in SPFx-Lösungen hinzu. Danach nichts mehr: liest man 24H1 bis 26H1, erwähnt keines davon SPFx überhaupt. Das 26H1-Update bringt Modern People Card, Document Intelligence und eine Konfigurationsoption für den People Picker. Nichts zum Framework.

Sie werden auch Community-Beiträge finden, die eine deutlich höhere Obergrenze behaupten. Wir konnten keinen davon gegen eine Erstquelle bestätigen, deshalb werden wir keine Versionsnummer wiederholen, für die wir nicht geradestehen können.

Daraus folgt eine praktische Anweisung statt einer Zahl: prüfen Sie es an der Serverfarm, nicht im Artikel. Stellen Sie ein triviales Paket bereit, gebaut mit der Version, die Sie einsetzen wollen, in Ihrem eigenen App-Katalog, und sehen Sie nach, ob es lädt. Fünfzehn Minuten davon sind mehr wert als jede Tabelle, auch die obige, denn die Obergrenze ist eine Eigenschaft Ihres Builds.

Was immer Sie finden: rechnen Sie damit, dass es stillsteht. Entwerfen Sie so, als wäre die Version, die Sie haben, die Version, die bleibt.

Die Umgebung, die tatsächlich baut

Die erste Falle ist die Werkzeugkette, und sie erwischt fast jeden einmal.

Der aktuelle Yeoman-Generator hilft Ihnen nicht. Ab Version 1.13.0 erzeugt er nur noch Projekte für SharePoint Online. Installieren Sie den neuesten, und das On-Premises-Ziel wird schlicht nicht angeboten. Sie brauchen einen Generator, der alt genug ist, um noch zu fragen:

bash
npm install gulp-cli@2.3.0 --global
npm install yo@2.0.6 --global
npm install @microsoft/generator-sharepoint@1.10.0 --global

Danach wählt man im Generator-Ablauf das On-Premises-Ziel.

Node ist alt. SPFx 1.4.1 und 1.5.1 zielen standardmäßig auf Node 6 und 8. Die Dokumentation weist darauf hin, dass sie sich mit zwei Änderungen auf Node v12.18.1, v14.17.1 und v16.15.0 zum Laufen bringen lassen: graceful-fs auf v4 oder neuer setzen und node-sass durch sass ersetzen. Das ist der Unterschied zwischen einer Build-Maschine, die 2026 noch jemand aufsetzen kann, und einer, die niemand mehr aufsetzt.

Halten Sie all das im Repository fest. Die Generator-Version, die Node-Version, die gulp-cli-Version. In drei Jahren wird die Person, die das übernimmt, nicht raten, und es wird keinen Artikel geben, der es ihr sagt. Eine .nvmrc und ein kurzer CONTRIBUTING-Abschnitt kosten jetzt nichts und sparen später eine Woche.

Planen Sie den getrennten Fall ein. Das Erzeugen des Projekts braucht npm-Zugang. Erreichen Ihre Entwicklungsmaschinen die Registry nicht, brauchen Sie eine lokale, und Microsofts eigene Empfehlung ist beim Preis deutlich: mehr Software und ein erheblicher Wartungsaufwand. Kalkulieren Sie das ehrlich ein, statt es in Woche drei zu entdecken.

Die Build-Befehle sind die von gulp, die eine Stelle, an der die älteren Artikel noch recht haben:

bash
gulp serve
gulp bundle --ship
gulp package-solution --ship

Mit SharePoint sprechen

SPHttpClient ist der Weg. Er kümmert sich um das Request Digest und die Authentifizierung und verhält sich über alle SPFx-Versionen hinweg gleich, was ihn zum haltbarsten Stück im Werkzeugkasten macht.

Legen Sie ihn in einen Service. Nicht in eine Komponente.

typescript
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';

export interface IDocument {
  Id: number;
  Title: string;
  Modified: string;
}

// Built apart from the call that uses it, so it can be exercised without a
// client, a farm or a browser. It is also the line most likely to be wrong.
export function itemsUrl(webUrl: string, listTitle: string, top: number): string {
  // A list called "HR & Payroll" or "Q1 'draft' items" breaks this. The test
  // list is always called Documents, which is why it reaches production.
  var list = encodeURIComponent(listTitle.replace(/'/g, "''"));

  return webUrl + "/_api/web/lists/getbytitle('" + list + "')/items" +
    '?$select=Id,Title,Modified' +
    '&$orderby=Modified desc' +
    '&$top=' + top;
}

// One place decides what an HTTP status means. Components never see numbers.
// Not called describe, because mocha puts a global function of that name in
// every test file, and the collision confuses long before it fails.
export function describeStatus(status: number): string {
  if (status === 401) { return 'notAuthenticated'; }
  if (status === 403) { return 'accessDenied'; }
  if (status === 404) { return 'notFound'; }
  if (status === 429 || status === 503) { return 'throttled'; }
  return 'unknown';
}

export class DocumentService {
  constructor(
    private readonly client: SPHttpClient,
    private readonly webUrl: string
  ) {}

  public getRecent(listTitle: string, top: number): Promise<IDocument[]> {
    return this.client
      .get(itemsUrl(this.webUrl, listTitle, top), SPHttpClient.configurations.v1)
      .then(function (response: SPHttpClientResponse): Promise<any> {
        if (!response.ok) {
          return Promise.reject(describeStatus(response.status));
        }
        return response.json();
      })
      .then(function (json: { value: IDocument[] }): IDocument[] {
        return json.value;
      });
  }
}

Vier Details darin wiegen schwerer, als sie aussehen.

Die URL wird von einer eigenen Funktion gebaut. Das ist keine Ordnungsliebe: es ist der einzige Teil des Service, den man ohne Serverfarm testen kann, und der nächste Abschnitt stützt sich darauf.

Das einfache Anführungszeichen wird verdoppelt, bevor der Titel kodiert wird, und die Reihenfolge zählt. Die REST-Syntax von SharePoint setzt den Listentitel zwischen einfache Anführungszeichen, deshalb schließt eine Liste namens Q1 'draft' items die Zeichenkette zu früh; OData maskiert ein Anführungszeichen, indem es verdoppelt wird. encodeURIComponent lässt das Anführungszeichen absichtlich in Ruhe, danach zu verdoppeln wäre also zu spät.

$select ist ausdrücklich. Alles anzufordern und drei Felder zu benutzen, ist Bandbreite, die bei jedem Rendern bezahlt wird, und auf einer Serverfarm, die einige Tausend Menschen bedient, ist das nicht umsonst.

Die Promise-Kette ist keine Nostalgie. TypeScript 2.4, das nach ES5 kompiliert, gibt Ihnen weder async noch await ohne eine Generator-Laufzeit. Die Kette ist die Fassung, die immer kompiliert.

Wenn Sie eine freundlichere API möchten, funktioniert PnPjs, aber Sie müssen auf eine Version festnageln, die Ihr SPFx unterstützt, und diesen Pin als dauerhaft behandeln. Ein npm update, das Sie still nach vorne schiebt, erzeugt eine Lösung, die auf Ihrer Maschine kompiliert und auf dem Server scheitert, und das ist die schlimmste Sorte Fehler zum Nachvollziehen.

Komponenten, und welches React Sie tatsächlich bekommen

Die Dokumentation widerspricht sich hier, und es lohnt sich, das aufzulösen statt es zu umgehen.

Drei Erstquellen zeigen in dieselbe Richtung. Die SPFx-Plattform-Kompatibilitätstabelle gibt der Subscription Edition v1.0 bis v1.5. Die 23H1-Release-Note ergänzt Unterstützung für SPFx 1.5.1. Die 23H2-Release-Note ergänzt Unterstützung für React 16 und Office UI Fabric React 7 und erlaubt Entwicklern, "diese neueren Komponentenversionen in ihren SharePoint-Framework-Lösungen einzusetzen".

Eine Seite zeigt in die andere Richtung. Der SPFx-Entwicklerartikel für On-Premises behauptet weiterhin, Subscription Edition habe "genau dieselben Abhängigkeiten und Anforderungen an das SharePoint Framework wie SharePoint Server 2019", und verweist auf v1.4.1. Diese Seite ist nicht nachgezogen: sie widerspricht der Kompatibilitätstabelle auf derselben Website und beschreibt das Produkt so, wie es vor 23H1 war. Zwei aktuelle Quellen gegen eine veraltete sind kein Gleichstand, also gelten die Release Notes.

Daraus folgt etwas Konkretes. Hooks kamen in React 16.8. Auf einer Farm ab 23H2 wird React 16 unterstützt, Hooks stehen Ihnen also offen, sofern Sie React 16.8 oder neuer installieren und festnageln. Die Release-Note nennt die Hauptversion und nicht die Nebenversion, und genau deshalb nagelt man sie fest und hält den Pin fest.

Unterhalb von 23H2 und auf SharePoint 2019 und 2016 sind Sie auf React 15, und jede Komponente ist eine Klasse.

Die Klassenkomponente lohnt sich in jedem Fall, weil sie überall läuft: auf 1.4.1 wie auf 1.5.1, oberhalb wie unterhalb von 23H2. Wenn Sie etwas schreiben, das einen Aktualisierungsplan überdauern muss, den Sie nicht kontrollieren, ist das der Boden, und er kostet drei Dinge, die von Hand zu erledigen sind.

typescript
import * as React from 'react';
import { DocumentService, IDocument } from '../services/DocumentService';

export interface IRecentDocumentsProps {
  service: DocumentService;
  listTitle: string;
  top: number;
}

export interface IRecentDocumentsState {
  status: string;
  items: IDocument[];
}

export default class RecentDocuments extends React.Component<
  IRecentDocumentsProps,
  IRecentDocumentsState
> {
  private _mounted: boolean = false;
  private _request: number = 0;

  constructor(props: IRecentDocumentsProps) {
    super(props);
    this.state = { status: 'loading', items: [] };
  }

  public componentDidMount(): void {
    this._mounted = true;
    this._load();
  }

  public componentWillUnmount(): void {
    // There is no cleanup function to return here. Without this flag, a slow
    // list on a busy farm updates a component that is already gone every time
    // somebody navigates away mid-load.
    this._mounted = false;
  }

  public componentDidUpdate(previous: IRecentDocumentsProps): void {
    // Compare first. Reloading unconditionally is an infinite loop: load, set
    // state, re-render, load again. It is silent until somebody opens the
    // network tab.
    if (previous.listTitle !== this.props.listTitle || previous.top !== this.props.top) {
      this._load();
    }
  }

  private _load(): void {
    var self = this;
    var request = ++this._request;

    this.setState({ status: 'loading', items: [] });

    this.props.service
      .getRecent(this.props.listTitle, this.props.top)
      .then(function (items: IDocument[]): void {
        // A slower answer to an older request must never overwrite a newer one.
        if (!self._mounted || request !== self._request) { return; }
        self.setState({ status: items.length ? 'ready' : 'empty', items: items });
      })
      .catch(function (failure: string): void {
        if (!self._mounted || request !== self._request) { return; }
        self.setState({
          status: failure === 'accessDenied' ? 'accessDenied' : 'error',
          items: []
        });
      });
  }

  public render(): React.ReactElement<IRecentDocumentsProps> {
    if (this.state.status === 'loading') {
      return <p role="status">Loading recent documents.</p>;
    }
    if (this.state.status === 'accessDenied') {
      return (
        <p role="alert">
          You do not have access to this library. Ask the site owner for read access.
        </p>
      );
    }
    if (this.state.status === 'error') {
      return <p role="alert">The library could not be reached. Try again in a moment.</p>;
    }
    if (this.state.status === 'empty') {
      return <p>No documents have been added yet.</p>;
    }

    return (
      <ul>
        {this.state.items.map(function (item: IDocument): JSX.Element {
          return <li key={item.Id}>{item.Title}</li>;
        })}
      </ul>
    );
  }
}

Das Flag _mounted ersetzt die Aufräumfunktion, die ein moderner Hook zurückgeben würde.

Der Zähler _request ersetzt alles, wozu man sonst greifen würde, um Nebenläufigkeit zu behandeln. Zwei schnell aufeinanderfolgende Änderungen im Eigenschaftenbereich starten zwei Anfragen, und sie kommen in der Reihenfolge zurück, die die Serverfarm bestimmt. Ohne den Zähler kann die ältere Antwort zuletzt eintreffen und gewinnen.

Der Vergleich in componentDidUpdate ist der klassische Fehler bei Klassenkomponenten. Lässt man ihn weg, hat man eine Endlosschleife gegen die eigene Serverfarm gebaut.

Themes

In dieser Version gibt es keinen ThemeProvider-Service und kein Fluent UI v9. Was es gibt, sind Theme-Token in SCSS, und die genügen:

scss
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';

.recentDocuments {
  color: "[theme: bodyText, default: #333333]";
  background-color: "[theme: white, default: #ffffff]";
  padding: 12px;
}

.recentDocuments a {
  color: "[theme: themePrimary, default: #0078d4]";
}

.recentDocuments a:hover {
  color: "[theme: themeDarkAlt, default: #106ebe]";
}

.recentDocuments .meta {
  color: "[theme: neutralSecondary, default: #666666]";
}

Die Regel ist dieselbe, die auf jeder Plattform und in jeder Version gilt: die Lösung nimmt ihre Farben von der Website. Schreiben Sie einen Hexwert direkt hinein, und Sie haben genau das gebaut, was an dem Tag zerbricht, an dem jemand ein anderes Theme anwendet, und auf einer Serverfarm, die mehrere Abteilungen mit mehreren Themes bedient, ist dieser Tag nah.

Der Wert hinter default: ist das, was dort erscheint, wo kein Theme angewendet ist. Wählen Sie ihn so, dass die Web Part lesbar bleibt statt unsichtbar zu werden.

Berechtigungen sind Benutzererlebnis

Ein Fehlschlag ist nicht eine einzige Sache, und on-premises wiegt das schwerer als erwartet, weil Berechtigungen in einer Serverfarm mit geerbten Strukturen und Zielgruppenadressierung wirklich kompliziert sind.

Fünf Situationen, und in vier davon kann ein Mensch etwas tun:

  • Nicht angemeldet. Neu laden und erneut anmelden.
  • Zugriff verweigert. Den Websitebesitzer fragen. Das ist kein Fehler, den der Lesende verursacht hat, und er verdient eine Anweisung statt einer Entschuldigung.
  • Nicht gefunden. Die Liste wurde umbenannt oder gelöscht. Sagen Sie, welche gesucht wurde.
  • Drosselung oder Nichtverfügbarkeit. Die Serverfarm ist ausgelastet. Bieten Sie einen Weg an, es erneut zu versuchen.
  • Es ist nichts da. Die Abfrage hat funktioniert und die Antwort ist leer. Das ist überhaupt kein Fehlschlag.

Ordnen Sie sie einmal zu, im Service, so wie es der Code oben tut. Dann gestalten Sie jede einzelne. Eine Web Part, die in allen fünf Fällen "Ein Fehler ist aufgetreten" sagt, ist eine Web Part, die in jedem dieser Fälle ein Supportticket erzeugt.

Und Microsoft Graph

Diese Frage kommt immer wieder, und die übliche Abfuhr, Ihre Daten lägen auf Ihren Servern und Graph sei damit irrelevant, macht es sich zu leicht. Viele Serverfarmen stehen neben einem Microsoft-365-Tenant, und Profildaten von dort haben zu wollen, ist völlig vernünftig.

Die genaue Antwort nützt mehr. MSGraphClient und AadHttpClient wurden beide in SPFx 1.4.1 eingeführt, die Klassen existieren also in Ihrer Version. Zwei Dinge stehen im Weg.

Die Release Notes zu 1.4.1 beschreiben sie als Developer Preview, "verfügbar zur Vorschau-Nutzung innerhalb von SharePoint Online", und ausdrücklich "noch nicht für den Produktionseinsatz gedacht".

Entscheidender noch: das Berechtigungsmodell, auf dem sie beruhen, ist von Anfang bis Ende SharePoint-Online-Infrastruktur. Man deklariert webApiPermissionRequests in der package-solution.json; die Bereitstellung im App-Katalog erzeugt Berechtigungsanfragen; ein Administrator genehmigt sie auf der API-Zugriffsseite des SharePoint Admin Centers; die Gewährung wird an der Anwendung SharePoint Online Client Extensibility gespeichert, die Microsoft in jedem Entra ID bereitstellt. Eine eigene Serverfarm hat nichts davon, es gibt also niemanden, bei dem man anfragt, und niemanden, dem man gewährt.

Der naheliegende Umweg ist ebenfalls versperrt: die Dokumentation stellt fest, dass die direkte Nutzung der Microsoft Authentication Library mit SPFx ab v1.4.1 nicht unterstützt wird.

Um genau zu sein, was feststeht und was nicht: dokumentiert ist, dass die Clients Vorschau waren und auf SharePoint Online beschränkt, dass ihr Berechtigungsmechanismus ein SharePoint-Online-Dienst ist und dass MSAL nicht unterstützt wird. Nirgends dokumentiert, soweit wir gesucht haben, ist ein unterstützter On-Premises-Weg. Fehlende Dokumentation beweist nicht, dass nichts funktioniert, aber eine Produktionslösung auf einem undokumentierten Weg, in einer Serverfarm, die Sie neun Jahre lang patchen müssen, ist eine Entscheidung, die man mit offenen Augen trifft.

Das Muster, das hält, ist ein eigener Service. Eine API, bereitgestellt, wo Sie sie beherrschen, gesichert, wie Sie es wollen, die serverseitig mit Graph spricht und die Ihre Web Part mit SPHttpClient oder einem einfachen HttpClient aufruft. Der Umgang mit Token hört auf, ein Problem des Browsers zu sein, und die Lösung bleibt innerhalb des Unterstützten.

Tests: der Runner ist bereits im Kasten

Üblicherweise wird angenommen, dass es hier keinen Test-Runner gibt. Microsofts eigener CI/CD-Artikel ist der Ursprung dieser Annahme, und es lohnt sich, den Satz genau zu lesen: "SharePoint Framework stellt standardmäßig kein Test-Framework bereit (seit 1.8.0)".

Seit 1.8.0. Darunter stellt es eines bereit, und die Version, die Subscription Edition akzeptiert, liegt darunter.

In 1.5.1 hängt das Build-Paket @microsoft/sp-build-web von @microsoft/gulp-core-build-karma ab, das Karma, Mocha, Chai, Sinon, sinon-chai, karma-coverage, den Istanbul-Instrumentierer und PhantomJS mitbringt. Der Generator legt @types/chai und @types/mocha in das Projekt, das er für Sie schreibt. Eine Aufgabe gulp test wartet, dokumentiert als "führt Unittests aus, falls vorhanden".

Was fehlt, ist nicht die Maschinerie. Es ist eine Konfigurationsdatei und überhaupt irgendein Test. Niemand sagt Ihnen das, deshalb wirkt gulp test kaputt und alle schließen daraus, dass da nichts ist.

Drei Dinge, auf die nichts hinweist

Die Aufgabe tut nichts, solange keine Konfigurationsdatei existiert. Sie sucht ./karma.config.js im Projektstamm. Fehlt die Datei, schreibt sie eine Warnung und kehrt zurück, und der Build bleibt grün. Schreiben Sie sie einmal:

bash
gulp test --initkarma

Das kopiert die Standardkonfiguration in Ihr Projekt, und von da an läuft gulp test tatsächlich.

Der Runner liest das Kompilat, nicht Ihre Quellen. Das Standardmuster ist /.+\.test\.js?$/, angewandt auf den Ordner lib. Die Aufgabe erzeugt temp/tests.js mit einem webpack-require.context über lib, und Karma lädt diese eine Datei. Ein Test, den Sie in src/webparts/recentDocuments/services/DocumentService.test.ts schreiben, wird also als lib/webparts/recentDocuments/services/DocumentService.test.js gefunden, nachdem TypeScript gelaufen ist. Zwei Folgen: nennen Sie die Datei .test.ts und nichts anderes (eine .spec.ts wird still ignoriert, ohne Fehler und ohne Warnung), und denken Sie daran, dass ein veralteter lib-Ordner veraltete Tests ausführt. gulp clean, sobald ein Ergebnis Sie überrascht.

Fehlschlagende Tests lassen den Build nicht scheitern. failBuildOnErrors steht standardmäßig auf false, ein roter Test schreibt also eine Warnung und der Build läuft weiter. Der Produktionsmodus ist die Ausnahme: mit --ship hält der Fehlschlag ihn an. Diesen Standardwert zu ändern lohnt sich am ersten Tag, denn eine Warnung in einer Wand von Build-Ausgabe ist eine Testsuite, die niemand liest. In der gulpfile.js, vor build.initialize(gulp):

javascript
build.karma.setConfig({ failBuildOnErrors: true });

Der Test

Karma lädt die Frameworks mocha und sinon-chai, describe und it sind also global und Chai steht zum Importieren bereit. Nichts braucht einen Renderer, weil nichts, was hier zu testen lohnt, rendert.

typescript
// src/webparts/recentDocuments/services/DocumentService.test.ts
import { expect } from 'chai';
import { itemsUrl, describeStatus } from './DocumentService';

describe('itemsUrl', () => {
  // Everybody develops against a list called Documents. Production has
  // "HR & Payroll" and "Q1 'draft' items", and that is where this breaks.
  it('doubles a single quote, which OData would read as the end of the title', () => {
    expect(itemsUrl('https://sp/sites/hr', "Q1 'draft' items", 5))
      .to.contain("getbytitle('Q1%20''draft''%20items')");
  });

  it('encodes an ampersand, which would otherwise start a new parameter', () => {
    expect(itemsUrl('https://sp/sites/hr', 'HR & Payroll', 5))
      .to.contain("getbytitle('HR%20%26%20Payroll')");
  });

  it('asks only for the three fields it renders', () => {
    expect(itemsUrl('https://sp', 'Documents', 5))
      .to.contain('$select=Id,Title,Modified');
  });
});

describe('describeStatus', () => {
  it('separates the failures a person can act on', () => {
    expect(describeStatus(401)).to.equal('notAuthenticated');
    expect(describeStatus(403)).to.equal('accessDenied');
    expect(describeStatus(404)).to.equal('notFound');
  });

  it('reads a busy farm as throttled rather than broken', () => {
    expect(describeStatus(429)).to.equal('throttled');
    expect(describeStatus(503)).to.equal('throttled');
  });

  it('does not pretend to recognise a status it has never seen', () => {
    expect(describeStatus(500)).to.equal('unknown');
  });
});
bash
gulp test                     # once, with coverage
gulp test --match itemsUrl    # passed through to mocha as a grep
gulp test --debug             # leaves the browser open, skips instrumentation

PhantomJS, der Teil, der Sie wirklich aufhalten wird

Die Standardkonfiguration führt genau einen Browser auf, und das ist PhantomJS, dessen Entwicklung 2018 eingestellt wurde.

phantomjs-prebuilt lädt bei der Installation eine Binärdatei herunter, aus einer Tabelle mit vier Einträgen: linux x64, linux ia32, darwin und win32. Alles andere bekommt "für Ihre Plattform oder Architektur ist keine Binärdatei verfügbar", und die Installation scheitert. Achten Sie darauf, was darwin dort bedeutet, denn genau das sieht nach Funktionieren aus und funktioniert nicht: es gibt einen einzigen macOS-Build und der ist x86_64, auf Apple Silicon gelingt der Download also und Sie erhalten eine Intel-Binärdatei, die Rosetta braucht. Auf Linux arm64 gibt es überhaupt keinen Eintrag.

Der Download-Host ist ein GitHub-Release, und er ist austauschbar:

bash
PHANTOMJS_CDNURL=https://your-mirror/phantomjs npm install

Das ist dasselbe Problem der getrennten Maschinen wie weiter oben in diesem Artikel, und es hat dieselbe Form: eine Sache mehr zum Spiegeln, ehrlich einkalkuliert statt am Morgen des Builds entdeckt.

Wenn PhantomJS für Sie nicht tragfähig ist, und nach 2018 ist es das meist nicht, wechseln Sie den Browser, nicht das Framework. Mocha, Chai und sämtliche Tests oben bleiben genau so, wie sie sind; nur der Launcher in der karma.config.js wechselt, zu karma-chrome-launcher mit ChromeHeadless. Bedenken Sie, dass Karma hier auf 0.13 steht, welche Launcher-Version mitspielt, findet man also durch Ausprobieren heraus, nicht durch Lesen. Das ist ein überschaubarer Nachmittag und rührt keine Zeile Ihres Testcodes an.

Der andere Weg, wenn Sie den Browser lieber ganz herauslassen, ist Jest, auf das Microsofts CI/CD-Artikel direkt verweist und wohin SPFx ab 1.8.0 ohnehin gegangen ist. Für ein React-15-Projekt nennt diese Dokumentation das Preset: @voitanos/jest-preset-spfx-react15. Es läuft in Node, PhantomJS hört also auf, Ihr Problem zu sein, um den Preis einer zweiten Werkzeugkette neben gulp test.

Was zu testen ist und was nicht

Verdient seinen Platz:

  • Der Aufbau der URL. Ein Apostroph, ein Kaufmanns-Und, ein Leerzeichen, ein Akzent. Der wertvollste Test der ganzen Lösung, und es sind die drei Fälle oben.
  • Die Zuordnung der Zustände. Ein 403 ergibt einen anderen Zustand als ein 500, und ein 404 einen anderen als beide.
  • Das Lesen der Antwort. Ein fehlendes Feld, ein Datum, das sich nicht umwandeln lässt, eine leere Liste.

Verdient seinen Platz nicht: zu behaupten, React habe ein <ul> gerendert.

Und eine Disziplin, die schwerer wiegt als all das: zerstören Sie jeden Schutz absichtlich und prüfen Sie, dass ein Test rot wird. Löschen Sie das Flag _mounted oder den Vergleich in componentDidUpdate und lassen Sie die Suite laufen. Läuft alles weiter durch, haben Sie etwas Echtes gelernt: entweder ist dieser Schutz ungetestet, oder das Szenario übt ihn nicht aus, oder der Test misst die falsche Eigenschaft. Eine grüne Suite, die grün bleibt, nachdem man den Code sabotiert hat, sagt nicht, dass der Code richtig ist. Sie sagt gar nichts.

Barrierefreiheit

React 15 rendert dasselbe HTML wie React 19. Nichts davon hängt von Ihrer Version ab.

  • Eine Ergebnisliste ist ein <ul> aus <li>, kein Stapel von <div>.
  • Der Link trägt den Titel, damit jemand, der von Link zu Link navigiert, Dokumentnamen hört statt eines wiederholten "weiterlesen".
  • Zustandsänderungen gehören in eine Live Region, und genau das tun role="status" und role="alert" in der Komponente oben.
  • Jedes Bedienelement ist mit der Tastatur erreichbar und der Fokus ist sichtbar.
  • Nichts wird allein durch Farbe getragen. Ein Zustand braucht ein Wort.

Prüfen Sie es im Barrierefreiheitsbaum des Browsers, nicht mit dem Auge. Was ein Screenreader vorliest, ist nicht immer das, was das Markup nahelegt.

Weniger Abhängigkeiten, und warum das hier schwerer wiegt

Jede Bibliothek, die Sie hinzufügen, ist auf ein altes TypeScript und ein altes React festgenagelt, ob Sie wollen oder nicht, und sie bleibt es. Auf einem Tenant aktualisiert man still; auf einer Serverfarm ist ein Aktualisierungsfenster ein Änderungsantrag mit einem Wartungswochenende daran.

Bevor Sie irgendetwas hinzufügen, fragen Sie, ob SharePoint das schon tut. Ein Diagramm ist oft besser als Zahl mit einem proportionalen Balken. Ein PDF ist meist besser als der eigene Druck des Browsers mit einem Druck-Stylesheet, was obendrein auswählbaren Text ergibt.

Die Messung, die die Sache entscheidet, ist die Paketgröße bei einem sauberen Build, davor und danach.

Halten Sie den Eigenschaftenbereich langweilig

Zwei Fragen entscheiden, ob eine Einstellung dorthin gehört.

Stellt sie derjenige, der die Web Part hinzufügt, einmal ein, oder ändert der Lesende sie während der Nutzung? Im zweiten Fall gehört sie in die Oberfläche.

Ändert sie das Verhalten? Wird eine Eigenschaft angenommen und ignoriert, ist sie kein Platzhalter für eine künftige Version. Sie ist ein Fehler, den jemand melden wird, nachdem er ihr vertraut hat.

Vier Eigenschaften, die alle etwas tun, schlagen zwölf, von denen die meisten nichts tun, in jeder Version von allem.

Bereitstellung

Der App-Katalog auf Ihrer eigenen Serverfarm und die Workbench unter /_layouts/workbench.aspx auf einer Website, die Sie kontrollieren. Diese Workbench ist nicht die gehostete, die für SharePoint Online eingestellt wird; Ihre lebt so lange wie die Serverfarm.

Zwei Dinge, die von Anfang an in die Routine gehören. Versionieren Sie die .sppkg bewusst in der package-solution.json, statt sie für immer auf 1.0.0.0 zu lassen, denn ein App-Katalog, der für drei verschiedene Builds dieselbe Version anzeigt, ist ein Supportproblem, das nur auf seinen Moment wartet. Und führen Sie Buch darüber, welcher Build wo bereitgestellt ist, denn es gibt keinen Dienst, der es Ihnen sagt.

Die Zusammenfassung

Eine lange Bahn auf einem stillstehenden Unterbau ist kein schlechter Ort. Sie belohnt Entscheidungen, die jetzt billig und später teuer sind: die Aufrufe in einen Service legen, Fehler einmal zuordnen, die Farben von der Website nehmen, Zustandsaktualisierungen absichern, die reinen Teile testen, so wenig wie möglich hinzufügen.

Eine so geschriebene Web Part in 1.5.1 wird noch laufen, wenn sich das Supportfenster der Plattform 2035 schließt. Eine anders geschriebene überlebt ihren ersten Themewechsel nicht, und pflegen werden Sie sie in beiden Fällen.

Bevor Sie anfangen

  • Bestätigen Sie den Funktionsupdate-Stand der Serverfarm und prüfen Sie die SPFx-Obergrenze, indem Sie ein triviales Paket bereitstellen, statt einer Tabelle zu vertrauen
  • Prüfen Sie, ob die Farm auf 23H2 oder neuer steht, denn das entscheidet über React 16 und Hooks, und nageln Sie die React-Version in jedem Fall fest
  • Nageln Sie die Versionen von Generator, Node, gulp-cli und Yeoman fest und halten Sie sie im Repository fest
  • Klären Sie die Frage des npm-Zugangs vor dem ersten Tag, wenn die Maschinen getrennt sind
  • Legen Sie jeden Aufruf in einen Service und ordnen Sie HTTP-Status an einer Stelle den Zuständen zu
  • Maskieren Sie einfache Anführungszeichen und kodieren Sie Listentitel in jeder REST-URL
  • Sichern Sie Zustandsaktualisierungen mit einem Mount-Flag und einem Anfragezähler ab
  • Vergleichen Sie die Props, bevor Sie in componentDidUpdate neu laden
  • Verwenden Sie Theme-Token in SCSS, niemals einen Hexwert
  • Gestalten Sie für jeden Fehlschlag einen Zustand, auch den der leeren Antwort
  • Führen Sie gulp test --initkarma einmal aus, nennen Sie Testdateien .test.ts und setzen Sie failBuildOnErrors: true
  • Klären Sie die PhantomJS-Frage, bevor Sie Tests schreiben, durch Wechsel des Launchers oder Umstieg auf Jest
  • Testen Sie den Aufbau der URL, die Zuordnung der Zustände und das Lesen der Antwort, und zerstören Sie jeden Schutz, um zu belegen, dass der Test rot wird
  • Nageln Sie PnPjs auf eine Version fest, die Ihr SPFx unterstützt, und behandeln Sie diesen Pin als dauerhaft
  • Versionieren Sie das Paket bewusst und halten Sie fest, was wo bereitgestellt ist

Weiterlesen

Migrationen

SharePoint 2016, 2019, Subscription Edition und Online: was sich wirklich ändert

2016 und 2019 enden beide am 14. Juli 2026. Subscription Edition hat kein Enddatum und lässt sich nicht einmalig kaufen. Online hat Funktionen, die die anderen nie bekommen, und Subscription Edition hat zwei, die üblicherweise als unmöglich gelistet werden. Ein praktischer Vergleich, mit der Lizenzierung, die entscheidet.

·18 Min. Lesezeit
KI und Agenten

Ein Copilot-Agentenmanifest ist eine Sicherheitsgrenze. Hier ist der Prüfer dafür.

Jede Eigenschaft, die in einem Microsoft 365 Copilot Agentenmanifest den Geltungsbereich setzt, ist optional, und in sechs Fällen gewährt das Weglassen den größten statt den kleinsten Bereich. Ein JSON-Schema-Validator lässt solche Manifeste durch, weil an keinem etwas ungültig ist: Es fehlt etwas Optionales. Also haben wir die Prüfung geschrieben, die sie findet. Sie findet sechs Probleme in einem Manifest, das jeden anderen Test besteht.

·6 Min. Lesezeit
Entwicklung und Automatisierung

SPFx vor Version 1.0: drei unserer Web Parts in den offiziellen Microsoft-Samples

Die Developer Preview des SharePoint Framework erschien im August 2016. Version 1.0 kam im Februar 2017. Unser erster Beitrag zum offiziellen Sample-Repository von Microsoft 365 stammt vom Oktober 2016, fünf Monate bevor es eine 1.0 gab, auf der man bauen konnte. Drei unserer Web Parts liegen heute in diesem Repository, und dies ist der Zweck jeder einzelnen.

·4 Min. Lesezeit