文件选取器

使用文件选取器 v8 将使你可以使用解决方案中 M365 服务中使用的相同功能。 这意味着随着我们对服务的迭代和改进,这些新功能会为用户而出现!

这个新的“控件”是通过发布消息与之交互的托管在 Microsoft 服务中的页面。 页面可以嵌入在 iframe 中或作为弹出窗口托管。

只需向我显示示例代码

可在此处找到 7.2 选取器的文档

所需设置

若要运行示例或在解决方案中使用控件,需要创建 AAD 应用程序。 可以遵循这些步骤:

  1. 创建新的 AAD 应用注册,注意应用程序的 ID
  2. 在身份验证下,创建新的单页应用程序注册表
    1. 将重定向 URI 设置为 https://localhost (用于测试示例)
    2. 请确保访问令牌和 ID 令牌都得到检查
    3. 可以选择将此应用程序配置为多租户,但这已超出了本文的讨论范围
  3. 在 API 权限下
    1. 添加 Files.Read.AllSites.Read.All、离开 User.Read 图委派权限
    2. 为 SharePoint 委派权限添加 AllSites.ReadMyFiles.Read

如果在 SharePoint 框架 中进行开发,则可以在应用程序清单中使用资源“SharePoint”和“Microsoft Graph”请求这些权限

若要允许用户上传文件并在选取器体验中创建文件夹,需要请求对 Files.ReadWrite.AllSites.ReadWrite.AllAllSites.WriteMyFiles.Write 的访问权限。

权限

文件选取器始终使用委托的权限进行操作,因此只能访问当前用户已有权访问的文件和文件夹。

至少必须请求 SharePoint MyFiles.Read 权限才能从用户的 OneDrive 和 SharePoint 网站读取文件。

请查看下表,了解根据要执行的操作需要哪些权限。 此表中的所有权限都指委托的权限。

读取 写入
OneDrive SharePoint.MyFiles.Read

Graph.Files.Read
SharePoint.MyFiles.Write

Graph.Files.ReadWrite
SharePoint 网站 SharePoint.MyFiles.Read

Graph.Files.Read

SharePoint.AllSites.Read
SharePoint.MyFiles.Write

Graph.Files.ReadWrite

SharePoint.AllSites.Write
Teams 频道 Graph.ChannelSettings.Read.All 和 SharePoint.AllSites.Read Graph.ChannelSettings.Read.All 和 SharePoint.AllSites.Write

运作方式

若要使用控件,必须:

  1. 向托管在 /_layouts/15/FilePicker.aspx 的“控件”页发出 POST 请求。 使用此请求需要提供一些参数,关键参数是 选取器配置
  2. 使用 postMessage消息端口 在主机应用程序与控件之间设置消息传递。
  3. 建立通信通道后,必须响应各种“命令”,第一个命令是提供身份验证令牌。
  4. 最后,需要响应其他命令消息,以提供新的/不同的身份验证令牌、处理选取的文件或关闭弹出窗口。

以下各部分对每一步骤进行了解释。

我们还提供显示了与控件集成的不同方法的 各种示例

启动选取器

若要启动选取器,需要创建一个可以是 iframe 或弹出窗口的“窗口”。 创建窗口后,应构建窗体并使用定义的查询字符串参数将窗体 POST 到 URL {baseUrl}/_layouts/15/FilePicker.aspx

上述 {baseUrl} 值为目标 Web 的 SharePoint Web URL 或用户的 OneDrive。 一些示例包括:“https://tenant.sharepoint.com/sites/dev"或 “https://tenant-my.sharepoint.com"。

OneDrive 使用者配置

name 描述
权威 https://login.microsoftonline.com/consumers
范围 OneDrive.ReadWrite 或 OneDrive.Read
baseUrl https://onedrive.live.com/picker

请求令牌时, OneDrive.Read 将使用 或 OneDrive.ReadWrite 请求令牌时。 请求应用程序的权限时,将选择 Files.ReadFiles.ReadWrite 或 (或另一个 Files.X 范围) 。

// create a new window. The Picker's recommended maximum size is 1080x680, but it can scale down to
// a minimum size of 250x230 for very small screens or very large zoom.
const win = window.open("", "Picker", "width=1080,height=680");

// we need to get an authentication token to use in the form below (more information in auth section)
const authToken = await getToken({
    resource: baseUrl,
    command: "authenticate",
    type: "SharePoint",
});

// to use an iframe you can use code like:
// const frame = document.getElementById("iframe-id");
// const win = frame.contentWindow;

// now we need to construct our query string
// options: These are the picker configuration, see the schema link for a full explaination of the available options
const queryString = new URLSearchParams({
   filePicker: JSON.stringify(options),
   locale: 'en-us'
});

// Use MSAL to get a token for your app, specifying the resource as {baseUrl}.
const accessToken = await getToken(baseUrl);

// we create the absolute url by combining the base url, appending the _layouts path, and including the query string
const url = baseUrl + `/_layouts/15/FilePicker.aspx?${queryString}`);

// create a form
const form = win.document.createElement("form");

// set the action of the form to the url defined above
// This will include the query string options for the picker.
form.setAttribute("action", url);

// must be a post request
form.setAttribute("method", "POST");

// Create a hidden input element to send the OAuth token to the Picker.
// This optional when using a popup window but required when using an iframe.
const tokenInput = win.document.createElement("input");
tokenInput.setAttribute("type", "hidden");
tokenInput.setAttribute("name", "access_token");
tokenInput.setAttribute("value", accessToken);
form.appendChild(tokenInput);

// append the form to the body
win.document.body.append(form);

// submit the form, this will load the picker page
form.submit();

选取器配置

通过序列化包含所需设置的 json 对象,并将其追加到查询字符串值 (如 启动选取器 部分中所示) 来配置选取器。 还可以查看 完整架构。 至少必须提供身份验证、项和消息传递设置。

下面显示了一个最小设置对象示例。 这会在频道 27 上设置消息传递,让选取器知道我们可以提供令牌,并且我们希望“我的文件”选项卡代表用户的 OneDrive 文件。 此配置将使用“https://{tenant}-my.sharepoint.com”形式的 baseUrl;

const channelId = uuid(); // Always use a unique id for the channel when hosting the picker.

const options = {
    sdk: "8.0",
    entry: {
        oneDrive: {}
    },
    // Applications must pass this empty `authentication` option in order to obtain details item data
    // from the picker, or when embedding the picker in an iframe.
    authentication: {},
    messaging: {
        origin: "http://localhost:3000",
        channelId: channelId
    },
}

选取器设计为在给定实例中与 OneDrive 或 SharePoint 配合使用,只应包含其中一个条目部分。

本地化

文件选取器接口支持与 SharePoint 设置的语言相同的本地化。

若要设置文件选取器的语言,请使用 locale 查询字符串参数,设置为上面列表中的 LCID 值之一。

建立消息传递

创建窗口并提交窗体后,需要建立消息传递通道。 这用于从选取器接收命令并进行响应。

let port: MessagePort;

function initializeMessageListener(event: MessageEvent): void {
    // we validate the message is for us, win here is the same variable as above
    if (event.source && event.source === win) {

        const message = event.data;

        // the channelId is part of the configuration options, but we could have multiple pickers so that is supported via channels
        // On initial load and if it ever refreshes in its window, the Picker will send an 'initialize' message.
        // Communication with the picker should subsequently take place using a `MessageChannel`.
        if (message.type === "initialize" && message.channelId === options.messaging.channelId) {
            // grab the port from the event
            port = event.ports[0];

            // add an event listener to the port (example implementation is in the next section)
            port.addEventListener("message", channelMessageListener);

            // start ("open") the port
            port.start();

            // tell the picker to activate
            port.postMessage({
                type: "activate",
            });
        }
    }
};

// this adds a listener to the current (host) window, which the popup or embed will message when ready
window.addEventListener("message", messageEvent);

消息侦听器实现

解决方案必须处理来自选取器的各种消息,将其分类为通知或命令。 通知不需要响应,可以视为日志信息。 一个例外情况是下面突出显示的 page-loaded 通知,它将告诉你选取器已准备就绪。

命令要求你确认,并根据命令进行响应。 本部分演示作为事件侦听器添加到端口的 channelMessageListener 函数的示例实现。 接下来的章节将详细介绍通知和命令。

async function channelMessageListener(message: MessageEvent): Promise<void> {
    const payload = message.data;

    switch (payload.type) {

        case "notification":
            const notification = payload.data;

            if (notification.notification === "page-loaded") {
                // here we know that the picker page is loaded and ready for user interaction
            }

            console.log(message.data);
            break;

        case "command":

            // all commands must be acknowledged
            port.postMessage({
                type: "acknowledge",
                id: message.data.id,
            });

            // this is the actual command specific data from the message
            const command = payload.data;

            // command.command is the string name of the command
            switch (command.command) {

                case "authenticate":
                    // the first command to handle is authenticate. This command will be issued any time the picker requires a token
                    // 'getToken' represents a method that can take a command and return a valid auth token for the requested resource
                    try {
                        const token = await getToken(command);

                        if (!token) {
                            throw new Error("Unable to obtain a token.");
                        }

                        // we report a result for the authentication via the previously established port
                        port.postMessage({
                            type: "result",
                            id: message.data.id,
                            data: {
                                result: "token",
                                token: token,
                            }
                        });
                    } catch (error) {
                        port.postMessage({
                            type: "result",
                            id: message.data.id,
                            data: {
                                result: "error",
                                error: {
                                    code: "unableToObtainToken",
                                    message: error.message
                                }
                            }
                        });
                    }

                    break;

                case "close":

                    // in the base of popup this is triggered by a user request to close the window
                    await close(command);

                    break;

                case "pick":

                    try {
                        await pick(command);
    
                        // let the picker know that the pick command was handled (required)
                        port.postMessage({
                            type: "result",
                            id: message.data.id,
                            data: {
                                result: "success"
                            }
                        });
                    } catch (error) {
                        port.postMessage({
                            type: "result",
                            id: message.data.id,
                            data: {
                                result: "error",
                                error: {
                                    code: "unusableItem",
                                    message: error.message
                                }
                            }
                        });
                    }

                    break;

                default:
                    // Always send a reply, if if that reply is that the command is not supported.
                    port.postMessage({
                        type: "result",
                        id: message.data.id,
                        data: {
                            result: "error",
                            error: {
                                code: "unsupportedCommand",
                                message: command.command
                            }
                        }
                    });

                    break;
            }

            break;
    }
}

获取令牌

控件要求我们能够根据发送的命令为其提供身份验证令牌。 为此,我们创建一个方法,该方法接受命令并返回令牌,如下所示。 我们使用 包 @azure/msal-browser 来处理身份验证工作。

控件目前依赖于 SharePoint 令牌而不是 Graph,因此需要确保资源正确,并且不能将令牌用于 Graph 调用。

import { PublicClientApplication, Configuration, SilentRequest } from "@azure/msal-browser";
import { combine } from "@pnp/core";
import { IAuthenticateCommand } from "./types";

const app = new PublicClientApplication(msalParams);

async function getToken(command: IAuthenticateCommand): Promise<string> {
    let accessToken = "";
    const authParams = { scopes: [`${combine(command.resource, ".default")}`] };

    try {

        // see if we have already the idtoken saved
        const resp = await app.acquireTokenSilent(authParams!);
        accessToken = resp.accessToken;

    } catch (e) {

        // per examples we fall back to popup
        const resp = await app.loginPopup(authParams!);
        app.setActiveAccount(resp.account);

        if (resp.idToken) {

            const resp2 = await app.acquireTokenSilent(authParams!);
            accessToken = resp2.accessToken;

        } else {

            // throw the error that brought us here
            throw e;
        }
    }

    return accessToken;
}

选取的项目结果

选择项目后,选取器将通过消息传递通道返回所选项的数组。 虽然有一组可能返回的信息,但始终保证包括以下内容:

{
    "id": string,
    "parentReference": {
        "driveId": string
    },
    "@sharePoint.endpoint": string
}

使用此 URL 可以构造一个 URL 来发出 GET 请求,以获取有关所选文件所需的任何信息。 它通常采用以下形式:

@sharePoint.endpoint + /drives/ + parentReference.driveId + /items/ + id

需要包含具有适当权限的有效令牌才能读取请求中的文件。

上传文件

如果向用于选取器令牌的应用程序授予 Files.ReadWrite.All 权限,顶部菜单中将显示一个小组件,允许将文件和文件夹上传到 OneDrive 或 SharePoint 文档库。 无需进行其他配置更改,此行为由应用程序 + 用户权限控制。 请注意,如果用户无权访问要上传的位置,则选取器不会显示 选项。