Compartir a través de


Uso @pnp/sp de la biblioteca (PnPJS) con elementos web SharePoint Framework

Puede usar la biblioteca @pnp/sp al compilar elementos web de SharePoint Framework (SPFx). Esta biblioteca ofrece una API fluida para generar consultas REST intuitivas y permite la compilación de lotes y el almacenamiento en caché. Para obtener más información, vea la página principal del proyecto, que contiene vínculos a documentación, ejemplos y otros recursos para ayudarle a empezar.

Nota:

PnPJS es una solución de código abierto con una comunidad activa que ofrece su soporte. No hay ningún contrato de nivel de servicio para el soporte de la herramienta de código abierto de Microsoft.

Puede descargar el código fuente completo de este artículo desde el sitio de ejemplos.

Nota:

Antes de seguir los pasos que se indican en el artículo, asegúrese de configurar el entorno de desarrollo del elemento web del lado cliente de SharePoint.

Crear un proyecto

  1. Cree una carpeta para el proyecto con la consola que prefiera:

    md spfx-pnp-js-example
    
  2. Especifique esa carpeta:

    cd spfx-pnp-js-example
    
  3. Ejecute el generador de Yeoman para SPFx:

    yo @microsoft/sharepoint
    
  4. Escriba los valores siguientes cuando se le pida durante la configuración del nuevo proyecto:

    • spfx-pnp-js-example como el nombre de la solución (se conserva el valor predeterminado)
    • Descripción spfx-pnp-js-example como descripción de la solución (mantener el valor predeterminado)
    • SharePoint Online only (latest) (Solo SharePoint Online [más reciente]) como versión base de referencia de los paquetes
    • N para permitir el acceso a las API web únicas
    • WebPart como el componente que se va a crear
    • PnPJSExample como nombre del elemento web
    • Descripción PnPJSExample como descripción
    • React como el marco
  5. Abra el proyecto en el editor de código que prefiera. Las capturas de pantalla que se muestran aquí ilustran Visual Studio Code. Para abrir el directorio de Visual Studio Code, escriba lo siguiente en la consola:

    code .
    

    Proyecto abierto por primera vez en Visual Studio Code

  6. Configure la ubicación de su área de trabajo hospedada SharePoint modificando el valor initialPage en el archivo config/serve.json para que apunte a su espacio empresarial o sitio.

{
  "$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
  "port": 4321,
  "https": true,
  "initialPage": "https://{your tenant name}/sites/dev/_layouts/15/workbench.aspx"
}

Instalación y configuración @pnp/sp

Una vez creado el proyecto, debe instalar y configurar el @pnp/sp paquete. Además, usaremos la @pnp/logging extensión, pero es opcional. Estos pasos son comunes para cualquier tipo de proyecto (React, etc.).

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

Para SPFx versión 1.14.x o para aquellos que no admiten typescript v4.x

Nota: PnPjs versión 3.x solo se admite en SPFx versión 1.14 y posteriores y NodeJs versión 12.x y posteriores.

  1. Actualice el compilador de la pila rush a la versión 4.2. Esto se describe en este excelente artículo de Elio, pero los pasos se enumeran a continuación.

    • Desinstalar el compilador de pila rush existente (reemplace la x por la versión instalada en el archivo package.json): npm uninstall @microsoft/rush-stack-compiler-3.x
    • Instalar la versión 4.2: npm i @microsoft/rush-stack-compiler-4.2
    • Actualizar tsconfig.json para ampliar la configuración 4.2: "extends": "./node_modules/@microsoft/rush-stack-compiler-4.2/includes/tsconfig-web.json"
  2. Reemplace el contenido de gulpfile.js por:

    'use strict';
    
    const build = require('@microsoft/sp-build-web');
    
    build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
    
    var getTasks = build.rig.getTasks;
    build.rig.getTasks = function () {
      var result = getTasks.call(build.rig);
    
      result.set('serve', result.get('serve-deprecated'));
    
      return result;
    };
    
    // ********* ADDED *******
    // disable tslint
    build.tslintCmd.enabled = false;
    // ********* ADDED *******
    
    build.initialize(require('gulp'));
    

Actualizar onInit en PnPjsExampleWebPart.ts

Dado que la @pnp/sp biblioteca crea solicitudes REST, debe conocer la dirección URL para enviar estas solicitudes. Cuando se trabaja en SPFx, es necesario basarse en el objeto de contexto proporcionado por el marco de trabajo.

Hay dos formas de comprobar que ha configurado correctamente las solicitudes (en este ejemplo usaremos el método onInit).

  1. Abra el archivo src\webparts\spPnPjsExample\SpPnPjsExampleWebPart.ts y añada una declaración de importación para el archivo pnp de configuración de proyecto (más información sobre este archivo a continuación):

    import { getSP } from './pnpjsConfig';
    
  2. En el onInit() método , actualice el código para que aparezca como se indica a continuación. Agregue la llamada para inicializar la configuración del proyecto después de la super.onInit() llamada. Hacemos esto después de super.onInit() para asegurarse de que el marco tiene la oportunidad de inicializar todo lo necesario y de que estamos configurando la biblioteca una vez completados esos pasos.

    /**
    * Initialize the web part.
    */
    public async onInit(): Promise<void> {
      this._environmentMessage = this._getEnvironmentMessage();
    
      await super.onInit();
    
      //Initialize our _sp object that we can then use in other packages without having to pass around the context.
      // Check out pnpjsConfig.ts for an example of a project setup file.
      getSP(this.context);
    }
    
    

Agregar un archivo de configuración de proyecto

A continuación, vamos a crear un archivo de configuración de proyecto para PnPjs. Este archivo nos permite configurar las importaciones que necesitaremos para el proyecto, así como inicializar una instancia del objeto sp para su uso en cualquiera de nuestros otros componentes.

Anote todas las importaciones para los sitios web, las listas, los elementos y el procesamiento por lotes. En nuestro componente, realizaremos llamadas para obtener los elementos a partir de una biblioteca, por lo que necesitamos incluir esas importaciones para futuras referencias. Además, se crea una variable que contendrá la instancia configurada del SharePoint Querable que se creará con la instancia de fábrica. Si usted recuerda nuestra función onInit anterior, estamos llamando al getSP exportado con el contexto SPFx pasado como una propiedad. Al hacerlo, podemos establecer el contexto con la biblioteca PnPjs para poder realizar posteriormente llamadas a la API SharePoint. Las siguientes llamadas a getSP sin contexto devolverán el objeto que ya ha sido configurado.

En este ejemplo también se muestra cómo se puede agregar un behavior adicional a la instancia que permite el registro de todas las llamadas. Esto usará el registro predeterminado, pero podríamos expandirlo para incluir nuestras propias funciones de registro.

import { WebPartContext } from "@microsoft/sp-webpart-base";

// import pnp and pnp logging system
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;

export const getSP = (context?: WebPartContext): SPFI => {
  if (_sp === null && context != null) {
    //You must add the @pnp/logging package to include the PnPLogging behavior it is no longer a peer dependency
    // The LogLevel set's at what level a message will be written to the console
    _sp = spfi().using(SPFx(context)).using(PnPLogging(LogLevel.Warning));
  }
  return _sp;
};

Adición de un archivo de interfaz para el modelo de datos

Agregue un nuevo archivo en la raíz de la carpeta de componentes denominada interfaces.ts. Reemplace el contenido por las siguientes definiciones a las que hará referencia nuestro componente.

// create File item to work with it internally
export interface IFile {
  Id: number;
  Title: string;
  Name: string;
  Size: number;
}

// create PnP JS response interface for File
export interface IResponseFile {
  Length: number;
}

// create PnP JS response interface for Item
export interface IResponseItem {
  Id: number;
  File: IResponseFile;
  FileLeafRef: string;
  Title: string;
}

Actualizar el componente predeterminado

Por último, es necesario realizar un poco de limpieza para crear nuestro componente basado. En primer lugar, reemplace todo el contenido del archivo PnPjsExample.tsx por el código siguiente.

import * as React from 'react';
import styles from './PnPjsExample.module.scss';
import { IPnPjsExampleProps } from './IPnPjsExampleProps';

// import interfaces
import { IFile, IResponseItem } from "./interfaces";

import { Caching } from "@pnp/queryable";
import { getSP } from "../pnpjsConfig";
import { SPFI, spfi } from "@pnp/sp";
import { Logger, LogLevel } from "@pnp/logging";
import { IItemUpdateResult } from "@pnp/sp/items";
import { Label, PrimaryButton } from '@microsoft/office-ui-fabric-react-bundle';

export interface IAsyncAwaitPnPJsProps {
  description: string;
}

export interface IIPnPjsExampleState {
  items: IFile[];
  errors: string[];
}

export default class PnPjsExample extends React.Component<IPnPjsExampleProps, IIPnPjsExampleState> {
  private LOG_SOURCE = "🅿PnPjsExample";
  private LIBRARY_NAME = "Documents";
  private _sp: SPFI;

  constructor(props: IPnPjsExampleProps) {
    super(props);
    // set initial state
    this.state = {
      items: [],
      errors: []
    };
    this._sp = getSP();
  }

  public componentDidMount(): void {
    // read all file sizes from Documents library
    this._readAllFilesSize();
  }

  public render(): React.ReactElement<IAsyncAwaitPnPJsProps> {
    // calculate total of file sizes
    const totalDocs: number = this.state.items.length > 0
      ? this.state.items.reduce<number>((acc: number, item: IFile) => {
        return (acc + Number(item.Size));
      }, 0)
      : 0;
    return (
      <div className={styles.pnPjsExample}>
        <Label>Welcome to PnP JS Version 3 Demo!</Label>
        <PrimaryButton onClick={this._updateTitles}>Update Item Titles</PrimaryButton>
        <Label>List of documents:</Label>
        <table width="100%">
          <tr>
            <td><strong>Title</strong></td>
            <td><strong>Name</strong></td>
            <td><strong>Size (KB)</strong></td>
          </tr>
          {this.state.items.map((item, idx) => {
            return (
              <tr key={idx}>
                <td>{item.Title}</td>
                <td>{item.Name}</td>
                <td>{(item.Size / 1024).toFixed(2)}</td>
              </tr>
            );
          })}
          <tr>
            <td></td>
            <td><strong>Total:</strong></td>
            <td><strong>{(totalDocs / 1024).toFixed(2)}</strong></td>
          </tr>
        </table>
      </div >
    );
  }

  private _readAllFilesSize = async (): Promise<void> => {
    try {
      // do PnP JS query, some notes:
      //   - .expand() method will retrive Item.File item but only Length property
      //   - .get() always returns a promise
      //   - await resolves proimises making your code act syncronous, ergo Promise<IResponseItem[]> becomes IResponse[]

      //Extending our sp object to include caching behavior, this modification will add caching to the sp object itself
      //this._sp.using(Caching({store:"session"}));

      //Creating a new sp object to include caching behavior. This way our original object is unchanged.
      const spCache = spfi(this._sp).using(Caching({store:"session"}));

      const response: IResponseItem[] = await spCache.web.lists
        .getByTitle(this.LIBRARY_NAME)
        .items
        .select("Id", "Title", "FileLeafRef", "File/Length")
        .expand("File/Length")();

      // use map to convert IResponseItem[] into our internal object IFile[]
      const items: IFile[] = response.map((item: IResponseItem) => {
        return {
          Id: item.Id,
          Title: item.Title || "Unknown",
          Size: item.File?.Length || 0,
          Name: item.FileLeafRef
        };
      });

      // Add the items to the state
      this.setState({ items });
    } catch (err) {
      Logger.write(`${this.LOG_SOURCE} (_readAllFilesSize) - ${JSON.stringify(err)} - `, LogLevel.Error);
    }
  }

  private _updateTitles = async (): Promise<void> => {
    try {
      //Will create a batch call that will update the title of each item
      //  in the library by adding `-Updated` to the end.
      const [batchedSP, execute] = this._sp.batched();

      //Clone items from the state
      const items = JSON.parse(JSON.stringify(this.state.items));

      let res: IItemUpdateResult[] = [];

      for (let i = 0; i < items.length; i++) {
        // you need to use .then syntax here as otherwise the application will stop and await the result
        batchedSP.web.lists
          .getByTitle(this.LIBRARY_NAME)
          .items
          .getById(items[i].Id)
          .update({ Title: `${items[i].Name}-Updated` })
          .then(r => res.push(r));
      }
      // Executes the batched calls
      await execute();

      // Results for all batched calls are available
      for (let i = 0; i < res.length; i++) {
        //If the result is successful update the item
        //NOTE: This code is over simplified, you need to make sure the Id's match
        const item = await res[i].item.select("Id, Title")<{ Id: number, Title: string }>();
        items[i].Name = item.Title;
      }

      //Update the state which rerenders the component
      this.setState({ items });
    } catch (err) {
      Logger.write(`${this.LOG_SOURCE} (_updateTitles) - ${JSON.stringify(err)} - `, LogLevel.Error);
    }
  }
}

Ejecutar el ejemplo

Inicie el ejemplo y agregue el elemento web al área de trabajo hospedada en SharePoint (/_layouts/15/workbench.aspx) para verlo en acción.

gulp serve --nobrowser

Puede eliminar elementos existentes si selecciona el icono de cubo de basura o puede agregar elementos si inserta valores en los dos campos y selecciona Agregar.

Proyecto tal como aparece después de la primera ejecución

Pasos siguientes

La @pnp/sp biblioteca contiene una gran variedad de funcionalidad y extensibilidad. Para obtener ejemplos, instrucciones y sugerencias para usar y configurar la biblioteca, vea la Guía del desarrollador.

Vea también