SPFx Series: PnPjs gebruiken voor API-aanroepen

Als je al een tijdje SPFx-oplossingen bouwt, heb je waarschijnlijk wel van PnPjs gehoord. En als je het nog niet gebruikt, maak je je leven moeilijker dan nodig is. PnPjs is een open-source verzameling van Fluent-bibliotheken die de SharePoint REST API en delen van de Microsoft Graph API omhullen in schone, ketenwerkbare, type-veilige aanroepen. Geen gedoe meer met ruwe fetch-verzoeken of het handmatig opbouwen van querystrings.

In dit bericht loop ik je door alles wat je moet weten om aan de slag te gaan: welke versie je moet gebruiken, hoe je het instelt, en de drie belangrijkste manieren om je code te structureren.

Welke versie van PnPjs heb ik nodig?

Voordat je een enkele regel code schrijft, moet je de juiste versie kiezen. Doe je dit verkeerd, dan verspil je tijd met het opsporen van vreemde fouten.

Versie 2 is wat je nodig hebt als je SharePoint on-premises gebruikt (2016 of 2019), of als je SPFx-versie ouder is dan 1.12.1. Voor alles moderns en cloudgebaseerd, kijk dan naar v3 of v4.

Versie 3 ondersteunt SPFx 1.12.1 tot en met 1.17.4. Als je op 1.12.1 tot en met 1.14.0 zit, zijn er extra configuratiestappen vereist om TypeScript 4.x te laten werken. De volledige instructies staan op pnp.github.io/pnpjs/getting-started. Vanaf 1.15.0 werkt v3 gewoon.

Versie 4 is de huidige nieuwste versie en is waarvoor dit bericht is geschreven. Het vereist Node.js 18 en werkt daarom alleen met SPFx 1.18.0 en later. Het goede nieuws: de API is vrijwel identiek aan v3, dus upgraden is eenvoudig.

Installatie

Twee pakketten dekken de meeste gebruikssituaties:

npm install @pnp/sp @pnp/graph --save

@pnp/sp geeft je toegang tot de SharePoint REST API, en @pnp/graph dekt de Microsoft Graph API. Voeg ook het logging-pakket toe — je zult jezelf er dankbaar voor zijn tijdens de ontwikkeling:

npm install @pnp/logging --save

Context begrijpen

De grootste conceptuele verschuiving van PnPjs v2 naar v3/v4 is hoe je context instelt. In v2 was er een globale sp.setup()-aanroep die je eenmalig deed en daarna vergat. Dat patroon is verdwenen.

In v3/v4 maak je instanties aan met behulp van factory-interfaces: spfi voor SharePoint en graphfi voor Microsoft Graph. Beide hebben het SPFx-contextobject nodig om te weten wie je bent, in welke tenant je zit en hoe verzoeken moeten worden geverifieerd. Er zijn drie manieren om dit in je project te structureren. Ik loop ze alle drie door.

Optie 1: Als een lokale variabele

De eenvoudigste aanpak. Definieer je spfi-instantie direct in onInit() en gebruik het van daaruit. Dit werkt prima voor kleine, op zichzelf staande webonderdelen waar je niet veel bestanden hebt die API-aanroepen maken.

import { spfi, SPFx } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";

export default class HelloWorldWebPart extends BaseClientSideWebPart {

  protected async onInit(): Promise {
    await super.onInit();

    const sp = spfi().using(SPFx(this.context));

    // Gebruik sp hier direct, of geef het door aan je component via props
    const lists = await sp.web.lists();
    console.log(lists);
  }
}

Als je zowel SharePoint als Graph in hetzelfde project nodig hebt, moet je de SPFx-import aliassen. Zowel @pnp/sp als @pnp/graph exporteren een behavior met precies die naam, dus zonder alias klaagt de TypeScript-compiler:

import { spfi, SPFx as spSPFx } from "@pnp/sp";
import { graphfi, SPFx as graphSPFx } from "@pnp/graph";

protected async onInit(): Promise {
  await super.onInit();

  const sp = spfi().using(spSPFx(this.context));
  const graph = graphfi().using(graphSPFx(this.context));
}

Deze aanpak is snel op te zetten, maar wordt rommelig zodra je het sp-object via props door meerdere lagen van React-componenten begint door te sturen. Voor alles groter dan een enkelvoudig webonderdeel, gebruik optie 2 of 3.

Optie 2: Een configuratiebestand gebruiken

Dit is mijn standaardkeuze voor de meeste projecten. Je maakt een centraal bestand pnpjs-config.ts in je src-map dat alle instellingen verwerkt en een functie getSP() exporteert. Elk bestand in je project dat API-aanroepen moet doen, importeert en roept simpelweg die functie aan — geen context doorgeven, geen prop drilling.

Stap 1: Maak src/pnpjs-config.ts aan

import { WebPartContext } from "@microsoft/sp-webpart-base";
import { spfi, SPFI, SPFx } from "@pnp/sp";
import { LogLevel, PnPLogging } from "@pnp/logging";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/batching";

var _sp: SPFI | null = null;

export const getSP = (context?: WebPartContext): SPFI => {
  if (context != null) {
    _sp = spfi().using(SPFx(context)).using(PnPLogging(LogLevel.Warning));
  }
  return _sp;
};

Een paar dingen zijn het vermelden waard. De selectieve imports, @pnp/sp/webs, @pnp/sp/lists, enz., zijn heel belangrijk in v3/v4. PnPjs gebruikt tree shaking, dus alleen wat je expliciet importeert wordt gebundeld. Importeer niet alles blind. Importeer alleen wat je nodig hebt en houd je bundle lean.

Het PnPLogging-gedrag voegt API-aanroep-logging toe aan de browserconsole. Handig tijdens de ontwikkeling, maar verlaag het niveau of verwijder het volledig voordat je naar productie gaat.

Stap 2: Initialiseer vanuit onInit in je webonderdeel

// HelloWorldWebPart.ts
import { getSP } from "./pnpjs-config";
import { SPFI } from "@pnp/sp";

export default class HelloWorldWebPart extends BaseClientSideWebPart {
  private _sp: SPFI;

  protected async onInit(): Promise {
    await super.onInit();
    this._sp = getSP(this.context); // Eenmalig initialiseren met context
  }

  public render(): void {
    // ... je render-code
  }
}

Het patroon is eenvoudig: de eerste aanroep naar getSP(this.context) maakt de instantie aan en slaat deze op. Elke volgende aanroep naar getSP() zonder argumenten retourneert diezelfde opgeslagen instantie.

Als je ook Graph nodig hebt, breid het configuratiebestand uit met een functie getGraph(). Vanwege het eerder genoemde naamconflict moet je de SPFx-imports aliassen:

import { WebPartContext } from "@microsoft/sp-webpart-base";
import { spfi, SPFI, SPFx as spSPFx } from "@pnp/sp";
import { graphfi, GraphFI, SPFx as graphSPFx } from "@pnp/graph";
import { LogLevel, PnPLogging } from "@pnp/logging";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/batching";

var _sp: SPFI | null = null;
var _graph: GraphFI | null = null;

export const getSP = (context?: WebPartContext): SPFI => {
  if (context != null) {
    _sp = spfi().using(spSPFx(context)).using(PnPLogging(LogLevel.Warning));
  }
  return _sp;
};

export const getGraph = (context?: WebPartContext): GraphFI => {
  if (context != null) {
    _graph = graphfi().using(graphSPFx(context)).using(PnPLogging(LogLevel.Warning));
  }
  return _graph;
};

Initialiseer beide vervolgens in onInit:

protected async onInit(): Promise {
  await super.onInit();
  getSP(this.context);
  getGraph(this.context);
}

Optie 3: Een serviceklasse gebruiken

Voor grotere, complexere projecten waarbij je goede dependency injection en een duidelijke scheiding tussen je API-laag en je UI-laag wilt, is een serviceklasse het juiste patroon.

Het belangrijkste verschil hier is dat een service geen directe toegang heeft tot this.context vanuit het webonderdeel. In plaats daarvan werkt het met ServiceScope, de SPFx dependency injection-container, en verbruikt het PageContext en AadTokenProviderFactory daaruit.

De serviceklasse:

// services/SampleService.ts
import { ServiceKey, ServiceScope } from "@microsoft/sp-core-library";
import { PageContext } from "@microsoft/sp-page-context";
import { AadTokenProviderFactory } from "@microsoft/sp-http";
import { spfi, SPFI, SPFx as spSPFx } from "@pnp/sp";
import { graphfi, GraphFI, SPFx as gSPFx } from "@pnp/graph";
import "@pnp/sp/webs";
import "@pnp/sp/lists";

export interface ISampleService {
  getLists(): Promise;
}

export class SampleService implements ISampleService {
  public static readonly serviceKey: ServiceKey =
    ServiceKey.create("SPFx:SampleService", SampleService);

  private _sp: SPFI;
  private _graph: GraphFI;

  constructor(serviceScope: ServiceScope) {
    serviceScope.whenFinished(() => {
      const pageContext = serviceScope.consume(PageContext.serviceKey);
      const aadTokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey);

      this._sp = spfi().using(spSPFx({ pageContext }));
      this._graph = graphfi().using(gSPFx({ aadTokenProviderFactory }));
    });
  }

  public getLists(): Promise {
    return this._sp.web.lists();
  }
}

Het gebruiken in je webonderdeel:

// HelloWorldWebPart.ts
import { SampleService, ISampleService } from "./services/SampleService";

export default class HelloWorldWebPart extends BaseClientSideWebPart {
  private _sampleService: ISampleService;

  protected async onInit(): Promise {
    await super.onInit();
    this._sampleService = this.context.serviceScope.consume(SampleService.serviceKey);
  }

  public render(): void {
    // Geef _sampleService door aan je component via props
  }
}

Welke optie moet je gebruiken?

Hier is een eenvoudige manier om erover na te denken:

  • Lokale variabele → Kleine webonderdelen, snelle prototypes, of wanneer je de API alleen op een of twee plaatsen hoeft aan te roepen.
  • Configuratiebestand → De meeste projecten. Schoon, eenvoudig, geen boilerplate, gemakkelijk te begrijpen.
  • Serviceklasse → Grotere projecten die goede gelaagdheid, dependency injection, unit testing of meerdere webonderdelen die dezelfde servicelogica delen nodig hebben.