Compartilhar via


Usar @pnp/sp a biblioteca (PnPJS) com web parts Estrutura do SharePoint

Talvez você queira usar a biblioteca @pnp/sp, quando criar as web parts da SPFx (Estrutura do SharePoint). Essa biblioteca fornece uma API fluente para tornar intuitiva a construção das suas consultas REST, bem como aceita o envio em lotes e o armazenamento em cache. Para obter mais informações, confira a página inicial do projeto, na qual há links para a documentação, amostras e outros recursos para ajudá-lo a começar.

Observação

O PnPJS é uma solução de software livre com uma comunidade ativa de suporte. Não há nenhuma SLA para o suporte da ferramenta de software livre por parte da Microsoft.

Você pode baixar a fonte completa deste artigo no site de amostras.

Criar um novo projeto

  1. Crie uma nova pasta para o projeto usando seu console preferido:

    md spfx-pnp-js-example
    
  2. Entre na pasta:

    cd spfx-pnp-js-example
    
  3. Execute o gerador Yeoman do SPFx:

    yo @microsoft/sharepoint
    
  4. Digite os seguintes valores quando solicitado durante a configuração do novo projeto:

    • spfx-pnp-js-example como o nome da solução (manter padrão)
    • Descrição spfx-pnp-js-example como a descrição da solução (mantenha o padrão)
    • Somente SharePoint Online (último) como a versão de pacotes de linha de base
    • N para permitir o acesso a APIs Web exclusivas
    • Web Part como o componente a ser criado
    • SPPnPJSExample como o nome da web part
    • Descrição PnPJSExample como a descrição
    • React como a estrutura
  5. Abra o projeto no editor de código de sua escolha. As capturas de tela mostradas aqui demonstram o Visual Studio Code. Para abrir a pasta dentro do Visual Studio Code, insira o seguinte no console:

    code .
    

    Projeto aberto pela primeira vez no Visual Studio Code

  6. Configure o local do seu workbench hospedado do SharePoint modificando o valor initialPage no config/serve.json para apontar para seu locatário/site.

{
  "$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"
}

Instalar e configurar @pnp/sp

Depois que seu projeto for criado, você deve instalar e configurar o @pnp/sp pacote. Além disso, usaremos a @pnp/logging extensão, mas isso é opcional. Essas etapas são comuns para qualquer tipo de projeto (React etc.).

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

Para SPFx versão 1.14.x ou aquelas que não dão suporte a typescript v4.x

Observação: PnPjs versão 3.x só tem suporte no SPFx versão 1.14 e superior e NodeJs versão 12.x e superior.

  1. Atualize o compilador do rush stack para 4.2. Isso é abordado neste excelente artigo do Elio, mas as etapas estão listadas abaixo.

    • Desinstale o compilador existente do rush stack (substitua o x pela versão instalada no arquivo package.json): npm uninstall @microsoft/rush-stack-compiler-3.x
    • Instale a versão 4.2: npm i @microsoft/rush-stack-compiler-4.2
    • Atualize tsconfig.json para estender a configuração 4.2: "extends": "./node_modules/@microsoft/rush-stack-compiler-4.2/includes/tsconfig-web.json"
  2. Substitua o conteúdo do 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'));
    

Atualizar onInit no PnPjsExampleWebPart.ts

Como a @pnp/sp biblioteca constrói solicitações REST, ela precisa saber a URL para enviar essas solicitações. Ao operar no SPFx, precisamos confiar no objeto de contexto fornecido pela estrutura.

Existem duas maneiras de garantir que você configurou corretamente suas solicitações; usaremos o método onInit neste exemplo.

  1. Abra o arquivo src\webparts\spPnPjsExample\SpPnPjsExampleWebPart.ts e adicione uma instrução de importação para o arquivo de configuração do projeto pnp (mais neste arquivo abaixo):

    import { getSP } from './pnpjsConfig';
    
  2. onInit() No método, atualize o código a ser exibido da seguinte maneira. Adicione a chamada para inicializar nossa configuração de projeto após a super.onInit() chamada. Fazemos isso depois do super.onInit() para garantir que a estrutura tenha a chance de inicializar tudo o que for necessário e que estamos configurando a biblioteca depois que essas etapas forem concluídas.

    /**
    * 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);
    }
    
    

Adicione um arquivo de configuração de projeto

Em seguida, vamos criar um arquivo de configuração de projeto para PnPjs. Esse arquivo nos permite configurar as importações necessárias para o projeto, bem como inicializar uma instância do objeto sp para uso em qualquer um de nossos outros componentes.

Observe todas as importações para webs, listas, itens e envio em lote. Em nosso componente, faremos chamadas para obter itens de uma biblioteca, portanto, precisamos incluir essas importações para referência futura. Além disso, criamos uma variável que conterá nossa instância configurada do SharePoint Querable que será criada com a instância de fábrica. Se você se lembrar de nossa função onInit acima, chamaremos o getSP exportado com o contexto SPFx passado como uma propriedade. Ao fazer isso, podemos estabelecer o contexto com a biblioteca PnPjs para que possamos fazer chamadas posteriormente para a API do SharePoint. Chamadas subsequentes para getSP sem o contexto retornarão o objeto que já foi configurado.

Este exemplo também mostra como podemos adicionar um behavior à instância que habilita o registro em log para todas as chamadas. Isso usará o log padrão, mas podemos expandir para incluir nossas próprias funções de registro em log.

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;
};

Adicione um arquivo de interface para o modelo de dados

Adicione um novo arquivo na raiz da pasta de componentes chamada interfaces.ts. Substitua o conteúdo pelas definições a seguir, que serão referenciadas por nosso 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;
}

Atualize o componente padrão

Por fim, precisamos fazer uma limpeza para criar nosso componente baseado. Primeiro, substitua todo o conteúdo do arquivo PnPjsExample.tsx pelo código a seguir.

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);
    }
  }
}

Executar o exemplo

Inicie a amostra e adicione a Web Part ao workbench hospedado do SharePoint (/_layouts/15/workbench.aspx) para vê-lo em ação.

gulp serve --nobrowser

Você pode excluir os itens existentes selecionando o ícone de cesto de lixo ou pode adicionar novos itens colocando valores em ambos os campos e selecionando Adicionar.

Projeto como aparece na primeira execução

Próximas etapas

A @pnp/sp biblioteca contém um grande intervalo de funcionalidades e extensibilidade. Para ver amostras, orientações e dicas sobre como usar e configurar a biblioteca, confira o Guia do Desenvolvedor.

Confira também