将代码从 SharePoint JavaScript 对象模型 (JSOM) 升级到客户端代码和 PnPjs

使用 SharePoint 外接程序模型开发解决方案时,你过去依赖 SharePoint JavaScript 对象模型 (JSOM) 从客户端代码使用 SharePoint Online。 例如,你曾经使用以下语法获取对客户端上下文的引用。

重要

本文指的是所谓的 PnP 组件、示例和/或工具,它们是由提供支持的活动社区支持的开源资产。 没有来自 Microsoft 的官方支持渠道的开放源代码工具支持的 SLA。 但是,这些组件或示例使用的是 Microsoft 支持的现成 API 和 Microsoft 支持的功能。

var context = SP.ClientContext.get_current();
var user = context.get_web().get_currentUser();

或者,你过去使用以下语法在目标 SharePoint Online 主机网站中获取库的项目。

// Get a reference to the current host web
var clientContext = SP.ClientContext.get_current();
var hostWebContext = new SP.AppContextSite(clientContext, hostweburl);
var hostweb = hostWebContext.get_web();

// Get a reference to the 'Documents' library
var list = hostweb.get_lists().getByTitle("Documents");

// Define a query to get all the items
var camlQuery = SP.CamlQuery.createAllItemsQuery();
var docs = documentsLibrary.getItems(camlQuery);

// Load and execute the actual query
clientContext.load(docs);
clientContext.executeQueryAsync(
    // Success callback
    function() {
        // Iterate through the items and display their titles
        var docsEnumerator = docs.getEnumerator();
        while (docsEnumerator.moveNext()) {
            var doc = docsEnumerator.get_current();
            console.log(doc.get_item('Title'));
        }
    },
    // Failure callback
    function(sender, args) {
        console.log('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
    }
);

上述语法基于 JSOM (sp.js) ,与 SharePoint 外接程序模型体系结构非常紧密,例如,它依赖于 SharePoint 托管网站的 URL 中具有 SharePoint 令牌。

如果愿意,可以watch以下视频,而不是阅读整篇文章,你仍然可以将其视为更详细的参考。

将代码从 SharePoint JavaScript 对象模型 (JSOM) 升级到客户端代码和 PnPjs

在 SharePoint 框架 中使用 SharePoint Online 数据

在 SharePoint Online 的新式开发模型中,JSOM 库不再是一个合适的选项,你应该依赖于 SharePoint Online REST API 或 Microsoft Graph API。 例如,如果要开发SharePoint 框架解决方案,则可以依赖于 SPFx 上下文的 SPHttpClientMSGraphClientV3 对象来分别使用 SharePoint REST API 或 Microsoft Graph API。

通过 SPHttpClient 使用 SharePoint Online 数据

例如,在以下代码摘录中,可以看到如何在 SPFx 中通过 SPHttpClient 使用上述示例的相同文档列表。

import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';

import styles from './ConsumeSpoViaClientCodeWebPart.module.scss';

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

// Define interface for each list item
export interface IListItem {
  Title?: string;
  Id: number;
}

// Define interface for list item collection
export interface ISPListItems {
  value: IListItem[];
}

export interface IConsumeSpoViaClientCodeWebPartProps {
}

export default class ConsumeSpoViaClientCodeWebPart extends BaseClientSideWebPart<IConsumeSpoViaClientCodeWebPartProps> {

  private _docs: ISPListItems;

  public render(): void {
    // For each document in the list, render a <li/> HTML element
    let docsOutput = '';
    this._docs.value.forEach(d => { docsOutput += `<li>${d.Title}</li>`; });
    this.domElement.innerHTML = `<div class="${ styles.consumeSpoViaClientCode }"><ul>${docsOutput}</ul></div>`;
  }

  protected async onInit(): Promise<void> {
    // Load all the documents onInit
    this._docs = await this._getDocuments();
    return super.onInit();
  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  // Get list items using spHttpClient
  private _getDocuments = async (): Promise<ISPListItems> => {
    // Get the REST response of the SharePoint REST API and return as collection of items
    return this.context.spHttpClient.get(this.context.pageContext.web.absoluteUrl + 
        `/_api/web/lists/GetByTitle('Documents')/items`, 
        SPHttpClient.configurations.v1)
      .then((response: SPHttpClientResponse) => {
        return response.json();
    });
  }
}

代码取自SharePoint 框架 Web 部件,该部件显示当前网站的“文档”库中的文件列表。

请注意,您不必依赖任何查询字符串令牌或参数,只需查询 this.context.spHttpClient 即可向 SharePoint REST API 发出 HTTP GET 请求,以访问“文档”文档库的项目。 还可以使用相同的 this.context.spHttpClient 对象通过 提取 方法发出 POST HTTP 请求或任何其他 HTTP 请求。 但是,尽管代码非常简单且微不足道,但你需要了解要调用的 SharePoint REST API URL 以及响应的 JSON 结构,这在某些情况下可能是一个挑战。

然而,使用上述技术,你基本上可以执行所需的任何操作,只需通过 REST 使用 SharePoint Online。

注意

通过阅读连接到 SharePoint API 一文,可以深入了解在 SharePoint 框架 中使用 SharePoint Online REST API

通过 MSGraphClient 使用 SharePoint Online 数据

另一种选择是使用 Microsoft 图形 API使用 SharePoint Online 数据。 在这里,可以找到使用相同文档列表但使用 Microsoft Graph 和 MSGraphClientV3 对象的 Web 部件的示例代码摘录。

import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';

import styles from './ConsumeSpoViaGraphWebPart.module.scss';

import { MSGraphClientV3 } from '@microsoft/sp-http';

// Define interface for each list item
export interface IListItem {
  name?: string;
  id: number;
}

// Define interface for list item collection
export interface ISPListItems {
  value: IListItem[];
}

export interface IConsumeSpoViaGraphWebPartProps {
}

export default class ConsumeSpoViaGraphWebPart extends BaseClientSideWebPart<IConsumeSpoViaGraphWebPartProps> {

  private _docs: ISPListItems;

  public render(): void {
    // For each document in the list, render a <li/> HTML element
    let docsOutput = '';
    this._docs.value.forEach(d => { docsOutput += `<li>${d.name}</li>`; });
    this.domElement.innerHTML = `<div class="${ styles.consumeSpoViaGraph }"><ul>${docsOutput}</ul></div>`;
  }

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

    // Load all the documents onInit
    this._docs = await this._getDocuments();
  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  // Get list items using spHttpClient
  private _getDocuments = async (): Promise<ISPListItems> => {
    // Get the REST response of the SharePoint REST API and return as collection of items
    const graphClient: MSGraphClientV3 = await this.context.msGraphClientFactory.getClient("3");
    return graphClient.api(`/sites/${this.context.pageContext.site.id}/drive/root/children`)
      .version('v1.0')
      .get();
  }
}

SPHttpClient 一样,语法并不过于复杂,并且通过了解所需的 Microsoft 图形 API 终结点以及 JSON 响应的结构,您可以轻松地使用 SharePoint Online 或 Microsoft 365 生态系统中任何其他服务中的任何数据,只要您拥有对SharePoint 框架解决方案的适当权限。

注意

阅读使用 MSGraphClientV3 连接到 Microsoft Graph 一文,可以在 SharePoint 框架 中使用 Microsoft 图形 API。

PnPjs 库简介

PnPjs 是一个开放源代码客户端库,由社区社区实现,它提供一系列流畅的库,用于以类型安全的方式使用 SharePoint Online、Microsoft Graph 和 Microsoft 365 REST API。 可以在SharePoint 框架解决方案中使用 PnPjs,在 Node.js 模块 (,例如脚本、Azure Functions等 ) ,在任何 JavaScript 或基于客户端的解决方案中。

通过 PnPjs 使用 SharePoint Online 数据

若要在SharePoint 框架 Web 部件中使用 PnPjs 的 SharePoint Online 数据,需要通过 npm 导入 PnPjs 提供的一个或多个库。 让我们创建一个SharePoint 框架 Web 部件项目并分步执行。

首先,你需要搭建SharePoint 框架解决方案的基架,因此启动命令提示符或终端窗口,创建一个文件夹,并从新创建的文件夹内运行以下命令。

重要

为了能够遵循演示的过程,需要在开发环境中安装SharePoint 框架。 阅读文档设置SharePoint 框架开发环境,可以找到有关如何设置环境的详细说明。

yo @microsoft/sharepoint

PowerShell 窗口中基架工具的 UI,同时为SharePoint 框架新式 Web 部件创建新项目。

按照提示为新式 Web 部件搭建解决方案的基架。 具体而言,在工具提示时做出以下选择:

  • 解决方案名称是什么? spo-sp-fx-pn-pjs
  • 要创建哪种类型的客户端组件? WebPart
  • Web 部件名称是什么? UsePnPjsMinimal
  • 你想要使用哪个模板? 最小

使用上述答案,你决定创建名为 spo-sp-fx-pn-pjs 的解决方案,其中将有一个 Web 部件,其名称为 UsePnPjsMinimal ,该部件将基于 最小 模板,这意味着它将仅基于 HTML、CSS 和 JavaScript 代码。

基架工具将为你生成新的SharePoint 框架解决方案。 完成后,只需使用喜欢的代码编辑器打开当前文件夹即可。 但是,在打开解决方案之前,需要通过运行以下命令添加 PnPjs 包:

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

上述命令在当前解决方案中同时安装 @pnp/sp@pnp/graph 包, @pnp/logging 以便进行日志记录。 总体而言,PnPjs 的可用包包括:

@pnp/
核心 跨所有 pnp 库提供共享功能
提供用于使用 Microsoft Graph 的流畅 API
测 井 轻型、可订阅的日志记录框架
msaljsclient 提供适合与 PnPjs 一起使用的 msal 包装器
nodejs 提供在 @pnp nodejs 中启用库的功能
可查询 提供共享查询功能和基类
Sp 提供用于使用 SharePoint REST 的流畅 API
sp-admin 提供用于使用 M365 租户管理方法的流畅 API

现在可以在喜欢的代码编辑器中打开解决方案。 如果你最喜欢的代码编辑器是 Microsoft Visual Studio Code,只需运行以下命令:

code .

首先,需要导入使用 SharePoint Online 数据所需的 PnPjs 类型。 因此,打开 Web 部件源文件并添加以下 import 语句。

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

第一个 import 语句导入 PnPjs 的初始化类型,而后续的导入语句只是导入使用 Web 对象、列表对象和列表项对象所需的类型。 完成此操作后,可以实现类似于以下代码摘录中的方法,以在“文档”库中加载文档。

// Get list items using PnPjs
private _getDocuments = async (): Promise<IListItem[]> => {

  // Initialized PnPjs
  const sp = spfi().using(SPFx(this.context));
  const items: IListItem[] = await sp.web.lists.getByTitle('Documents').items();
  
  return items;
}

如你所看到的,语法非常简单明了。 事实上,代码初始化 SPFI 类型对象的新实例, (代表 SharePoint 工厂接口,) 提供SharePoint 框架的上下文对象,其中 SPFI 是一种 PnPjs 类型。 然后,使用刚刚初始化的 sp 对象,它依赖于流畅的语法来收集当前 Web 中标题为“Documents”的列表项。

在以下代码摘录中,可以看到 Web 部件的整个代码。

import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';

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

import styles from './UsePnPjsMinimalWebPart.module.scss';

// Define interface for each list item
export interface IListItem {
  Title?: string;
  Id: number;
}

export interface IUsePnPjsMinimalWebPartProps {
}

export default class UsePnPjsMinimalWebPart extends BaseClientSideWebPart<IUsePnPjsMinimalWebPartProps> {

  private _docs: IListItem[];

  public render(): void {
    // For each document in the list, render a <li/> HTML element
    let docsOutput = '';
    this._docs.forEach(d => { docsOutput += `<li>${d.Title}</li>`; });
    this.domElement.innerHTML = `<div class="${ styles.usePnPjsMinimal }"><ul>${docsOutput}</ul></div>`;
  }

  protected async onInit(): Promise<void> {
    // Load all the documents onInit
    this._docs = await this._getDocuments();
    return await super.onInit();
  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  // Get list items using PnPjs
  private _getDocuments = async (): Promise<IListItem[]> => {

    // Initialized PnPjs
    const sp = spfi().using(SPFx(this.context));
    const items: IListItem[] = await sp.web.lists.getByTitle('Documents').items();
    
    return items;
  }
}

PnPjs 的流畅语法还提醒你在经典 SharePoint 外接程序模型中用于 CSOM 或 JSOM 的语法。

在React Web 部件中使用 PnPjs

现在,你已了解如何在基本 JavaScript 代码中读取 SharePoint 数据,让我们转到更真实、更常见的用例,即在具有 SharePoint 框架 的 React Web 部件中使用 PnPjs。

打开命令提示符并转到上一个 SPFx 解决方案的同一文件夹,然后再次运行 SPFx 基架工具,运行以下命令。

yo @microsoft/sharepoint

针对同一解决方案多次执行基架工具时,它将允许向现有解决方案添加其他项目或组件。

PowerShell 窗口中基架工具的 UI,同时为SharePoint 框架新式 Web 部件创建新项目。

按照提示为新式 Web 部件搭建解决方案的基架。 具体而言,在工具提示时做出以下选择:

  • 要创建哪种类型的客户端组件? WebPart
  • Web 部件名称是什么? UsePnPjsReact
  • 你想要使用哪个模板? React

使用上述答案,你决定向解决方案添加另一个 Web 部件。 新的 Web 部件名称将为 UsePnPjsReact,它将使用 UI/UX 的 React 模板。

现在,可以像前面的示例一样初始化 PnPjs SPFI 对象,并将其传递给将 Web 部件呈现为自定义属性的 React 组件。 例如,定义 React 组件属性的接口可能类似于以下代码中所示。

import { SPFI } from "@pnp/sp";

export interface IUsePnPjsReactProps {
  description: string;
  isDarkTheme: boolean;
  environmentMessage: string;
  hasTeamsContext: boolean;
  userDisplayName: string;
  sp: SPFI;
}

Web 部件可以初始化 React 组件,如以下代码摘录所示。

export default class UsePnPjsReactWebPart extends BaseClientSideWebPart<IUsePnPjsReactWebPartProps> {

  private _isDarkTheme: boolean = false;
  private _environmentMessage: string = '';
  private _sp: SPFI;

  public render(): void {
    const element: React.ReactElement<IUsePnPjsReactProps> = React.createElement(
      UsePnPjsReact,
      {
        description: this.properties.description,
        isDarkTheme: this._isDarkTheme,
        environmentMessage: this._environmentMessage,
        hasTeamsContext: !!this.context.sdks.microsoftTeams,
        userDisplayName: this.context.pageContext.user.displayName,
        sp: this._sp
      }
    );

    ReactDom.render(element, this.domElement);
  }

  protected async onInit(): Promise<void> {
    // Initialized PnPjs
    this._sp = spfi().using(SPFx(this.context));

    return this._getEnvironmentMessage().then(message => {
      this._environmentMessage = message;
    });
  }

  // Omitted code, for the sake of simplicity ...

最后,在 React 组件中,可以依赖组件属性中提供的 sp 属性来使用 PnPjs fluent 语法并检索目标库中的项。 下面是此逻辑的一个简化示例。

import * as React from 'react';
import styles from './UsePnPjsReact.module.scss';
import { IUsePnPjsReactProps } from './IUsePnPjsReactProps';
import { IUsePnPjsReactState } from './IUsePnPjsReactState';

import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";

export default class UsePnPjsReact extends React.Component<IUsePnPjsReactProps, IUsePnPjsReactState> {

  constructor(props: IUsePnPjsReactProps) {
    super(props);
    
    this.state = {
      documents: []
    }
  }

  override async componentDidMount(): Promise<void> {

    const docs = await this.props.sp.web.lists.getByTitle("Documents").items<{Id: number; Title: string;}[]>();

    this.setState({
      documents: docs
    });
  }

  public render(): React.ReactElement<IUsePnPjsReactProps> {
    const {
      isDarkTheme,
      hasTeamsContext
    } = this.props;

    const {
      documents
    } = this.state;

    return (
      <section className={`${styles.usePnPjsReact} ${hasTeamsContext ? styles.teams : ''}`}>
        <div className={styles.welcome}>
          <img alt="" src={isDarkTheme ? require('../assets/welcome-dark.png') : require('../assets/welcome-light.png')} className={styles.welcomeImage} />
        </div>
        <div>
          <h3>Here are the documents!</h3>
          <ul className={styles.links}>
            { documents.map(d => <li key={d.Id}>{d.Title}</li>)}
          </ul>
        </div>
      </section>
    );
  }
}

但是,在解决方案中,可能需要使用来自多个React组件的 PnPjs,并且将 SPFI 对象实例作为属性提供给所有组件不一定是最佳选择,也不一定是最佳做法。

若要提高代码质量,应在解决方案中创建一个包含以下内容的文件,例如将其称为 pnpjsConfig.ts

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

// eslint-disable-next-line no-var
var _sp: SPFI = null;

export const getSP = (context?: WebPartContext): SPFI => {
  if (!!context) { // eslint-disable-line eqeqeq
    //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;
};

文件导出一个函数,该函数基于提供的可选 SPFx 上下文生成 SPFI 的新实例。 如果在未提供上下文的情况下调用函数,它将尝试重用以前创建的 SPFI 实例(如果有)。

注意

可以通过阅读 Project Config/Services Setup 找到有关此模式的其他信息,并在 GitHub 上的示例解决方案 Using @pnp/js and ReactJS中找到此模式的完整功能示例。

定义 pnpjsConfig.ts 文件后,可以在 Web 部件类中导入该文件,并从 Web 部件的 onInit 方法中调用 getSP 方法,如以下代码摘录中所示。

import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
  IPropertyPaneConfiguration,
  PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';

import * as strings from 'UsePnPjsReactBetterWebPartStrings';
import UsePnPjsReactBetter from './components/UsePnPjsReactBetter';
import { IUsePnPjsReactBetterProps } from './components/IUsePnPjsReactBetterProps';

// Import the getSP function from the pnpjsConfig file
import { getSP } from '../../pnpjsConfig';

export interface IUsePnPjsReactBetterWebPartProps {
  description: string;
}

export default class UsePnPjsReactBetterWebPart extends BaseClientSideWebPart<IUsePnPjsReactBetterWebPartProps> {

  private _isDarkTheme: boolean = false;
  private _environmentMessage: string = '';

  public render(): void {
    const element: React.ReactElement<IUsePnPjsReactBetterProps> = React.createElement(
      UsePnPjsReactBetter,
      {
        description: this.properties.description,
        isDarkTheme: this._isDarkTheme,
        environmentMessage: this._environmentMessage,
        hasTeamsContext: !!this.context.sdks.microsoftTeams,
        userDisplayName: this.context.pageContext.user.displayName
      }
    );

    ReactDom.render(element, this.domElement);
  }

  protected onInit(): Promise<void> {

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

    return this._getEnvironmentMessage().then(message => {
      this._environmentMessage = message;
    });
  }

  // Omitted code, for the sake of simplicity ...

现在,无论在何处需要访问 PnPjs,都只需导入 getSP 函数并调用它,而无需提供任何参数来取回已初始化的 SPFI 对象实例。 例如,在解决方案的任何React组件中,可以编写如下语法。

import * as React from 'react';
import styles from './UsePnPjsReactBetter.module.scss';
import { IUsePnPjsReactBetterProps } from './IUsePnPjsReactBetterProps';
import { IUsePnPjsReactBetterState } from './IUsePnPjsReactBetterState';

import { SPFI } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";

import { getSP } from '../../../pnpjsConfig';

export default class UsePnPjsReactBetter extends React.Component<IUsePnPjsReactBetterProps, IUsePnPjsReactBetterState> {

  private _sp: SPFI;

  constructor(props: IUsePnPjsReactBetterProps) {
    super(props);

    this.state = {
      documents: []
    }
    
    this._sp = getSP();
  }

  override async componentDidMount(): Promise<void> {

    const docs = await this._sp.web.lists.getByTitle("Documents").items<{Id: number; Title: string;}[]>();

    this.setState({
      documents: docs
    });
  }

  public render(): React.ReactElement<IUsePnPjsReactBetterProps> {
    const {
      isDarkTheme,
      hasTeamsContext
    } = this.props;

    const {
      documents
    } = this.state;

    return (
      <section className={`${styles.usePnPjsReactBetter} ${hasTeamsContext ? styles.teams : ''}`}>
        <div className={styles.welcome}>
          <img alt="" src={isDarkTheme ? require('../assets/welcome-dark.png') : require('../assets/welcome-light.png')} className={styles.welcomeImage} />
        </div>
        <div>
          <h3>Here are the documents!</h3>
          <ul className={styles.links}>
            { documents.map(d => <li key={d.Id}>{d.Title}</li>)}
          </ul>
        </div>
      </section>
    );
  }
}

请注意构造函数中的语法,其中调用 了 getSP 函数。

this._sp = getSP();

此外,请注意检索到的 SPFI 实例的用法,例如在 componentDidMount 方法中。

const docs = await this._sp.web.lists.getByTitle("Documents").items<{Id: number; Title: string;}[]>();

在基于React的 Web 部件中使用 PnPjs 时,你刚才看到的模式非常常见,你应该在自己的解决方案中依赖它。

重要

在某些情况下,需要在支持业务逻辑的服务类中使用 PnPjs。 在这种情况下,不一定具有 React 组件,并且不一定依赖于 SPFx 上下文对象,除非将其作为输入参数提供给服务类(例如在服务类的构造函数中)。 但是,通常,将 SPFx 上下文作为构造函数参数或React组件属性传递并不是一种很好的设计模式。 如果需要在 SPFx 中创建依赖于 PnPjs 的服务类,可以参考 使用服务类 设计模式。

有关本主题的其他信息,请参阅以下文档: