在加载项 Web 中通过 JavaScript 处理主机 Web 数据

这是关于开发 SharePoint 托管的 SharePoint 加载项的基础知识系列文章中的第 11 篇文章。你应该首先熟悉 SharePoint 加载项以及本系列中之前的文章(可在开始创建 SharePoint 托管 SharePoint 加载项|后续步骤中找到相关内容)。

注意

如果已完成有关 SharePoint 托管加载项的本系列文章之一,便已生成 Visual Studio 解决方案,可以在继续阅读本主题的过程中使用。 也可以从 SharePoint_SP-hosted_Add Ins_Tutorials 下载存储库,再打开 BeforeHostWebData.sln 文件。

默认情况下,SharePoint 旨在阻止加载项中的 JavaScript 访问场中其他 SharePoint 网站的数据。 这样可以防止未授权加载项中的脚本访问敏感数据。 不过,加载项通常需要访问主机 Web,或访问与主机 Web 同属一个网站集的其他网站。

在加载项中启用此方案的过程分为以下两部分:

  • 在加载项的加载项清单文件中请求获取对主机 Web 的权限。 安装加载项的用户会看到授予此权限的提示。如果用户未授予,则无法安装加载项。
  • 使用 SP.AppContextSite 对象(而不是 SP.ClientContext 对象)向主机 Web 发出 JSOM 调用。 使用此对象,加载项可以获取除加载项 Web 以外网站的上下文对象,但仅限同一网站集内的网站。 (还有一种方法可用于访问 SharePoint Online 订阅中的任何网站[或本地 SharePoint Web 应用],但此为高级主题。)

在本文中,您可以使用 JSOM 查找尚未启动的方向,并确保它们已在主机 Web 中的日历上进行规划。

准备主机 Web 日历

打开主机 Web(开发人员测试网站),并确认其上是否有“员工入职培训计划”日历,且日历中是否有一个事件“Cassie Hicks 入职培训”。 如果没有,请按照以下步骤操作:

  1. 在网站的主页中,依次选择“网站内容”>“添加加载项”>“日历”

  2. 在“添加日历”对话框中,输入“员工入职培训计划”作为“名称”,再选择“创建”

  3. 在日历打开时,将光标悬停在任意日期之上,直到日期上显示“添加”链接,再选择“添加”

  4. 在“员工入职培训计划 - 新项”对话框中,输入“Cassi Hicks 入职培训”作为“标题”。 保留其他字段的默认设置,再选择“保存”

    日历应如下所示:

    图 1. 自定义日历

    在“员工入职培训计划”日历中,6 月 1 日上有“Cassi Hicks 入职培训”项

创建 JavaScript 以及调用它的按钮

  1. 在“解决方案资源管理器”中,展开“脚本”节点,打开 Add-in.js 文件。

  2. completedItems 的声明下方添加以下声明。

    var notStartedItems;
    var calendarList;
    var scheduledItems;
    
    • notStartedItems 引用“西雅图新员工”列表中“入职培训阶段”为“未启动”的项。
    • calendarList 引用在主机 Web 上创建的日历。
    • scheduledItems 引用日历中的一系列项。
  3. 当 SharePoint 加载项运行时,SharePoint 会调用它的起始页,并向起始页 URL 添加一些查询参数。 其中一个就是 SPHostUrl,顾名思义这是主机 Web URL。 加载项需要获取此信息,才能向主机 Web 数据发出调用。因此,在 Add-in.js 文件顶部附近的 scheduledItems 变量声明下方添加以下代码行。

    var hostWebURL = decodeURIComponent(getQueryStringParameter("SPHostUrl"));
    

    关于此代码,请注意以下几点:

    • getQueryStringParameter 是在下一步中创建的实用工具函数。
    • decodeUriComponent 是标准 JavaScript 函数,用于反向处理 SharePoint 对查询参数执行的 URI 编码;例如,将编码的正斜线“%2F”更改回“/”。
  4. 将以下代码添加到文件底部。 此函数可用于读取查询参数。

    // Utility functions
    
    function getQueryStringParameter(paramToRetrieve) {
        var params = document.URL.split("?")[1].split("&");
        var strParams = "";
        for (var i = 0; i < params.length; i = i + 1) {
            var singleParam = params[i].split("=");
            if (singleParam[0] == paramToRetrieve) {
                return singleParam[1];
          }
        }
    }
    
  5. 将以下函数添加到 Add-in.js 文件中出错回调部分上面的某处位置。

    function ensureOrientationScheduling() {
    
      var camlQuery = new SP.CamlQuery();
      camlQuery.set_viewXml(
          '<View><Query><Where><Eq>' +
              '<FieldRef Name=\'OrientationStage\'/><Value Type=\'Choice\'>Not started</Value>' +
          '</Eq></Where></Query></View>');
      notStartedItems = employeeList.getItems(camlQuery);
    
      clientContext.load(notStartedItems);
      clientContext.executeQueryAsync(getScheduledOrientations, onGetNotStartedItemsFail);
      return false;
    }
    

    关于此代码,请注意以下几点:

    • 这与获取“已完成”项的列表查询方法几乎完全相同,不同之处在于这种方法获取的是“未启动”项,而不是“已完成”项。 只关注“未启动”项,因为脚本是以简单化假设为依据,即如果入职培训已不再处于“未启动”阶段,一定已经过计划。
    • 在后续步骤中,通过 executeQueryAsync 调用创建两种回调方法。
  6. 将以下函数添加到 Add-in.js 文件中的上一函数正下方。 请注意,它使用 hostWebContext 对象标识所查询的列表。

    function getScheduledOrientations() {
    
      var hostWebContext = new SP.AppContextSite(clientContext, hostWebURL);
      calendarList = hostWebContext.get_web().get_lists().getByTitle('Employee Orientation Schedule');
    
      var camlQuery = new SP.CamlQuery();
      scheduledItems = calendarList.getItems(camlQuery);
    
      clientContext.load(scheduledItems);
      clientContext.executeQueryAsync(scheduleAsNeeded, onGetScheduledItemsFail);
    }
    

    注意

    请注意,未向 CAML 查询添加任何查询标记。 让查询对象中没有实际查询是为了确保检索所有列表项。 如果列表非常大,这可能会导致对服务器发出的请求长时间运行,时间之长令人无法接受。 在这种情况下,需要寻找其他方式来实现目标。 不过,此示例中的列表非常小(日历列表几乎都很小)。可以获取整个列表以便在客户端上遍历列表,这样其实有助于最大限度地减少对服务器的调用次数(即调用 executeQueryAsync 的次数)。

  7. 将以下函数添加到文件中。

    function scheduleAsNeeded() {
    
      var unscheduledItems = false;
      var dayOfMonth = '10';
    
      var listItemEnumerator = notStartedItems.getEnumerator();
    
      while (listItemEnumerator.moveNext()) {
          var alreadyScheduled = false;
          var notStartedItem = listItemEnumerator.get_current();
    
          var calendarEventEnumerator = scheduledItems.getEnumerator();
          while (calendarEventEnumerator.moveNext()) {
              var scheduledEvent = calendarEventEnumerator.get_current();
    
                // The SP.ListItem.get_item('field_name ') method gets the value of the specified field.
              if (scheduledEvent.get_item('Title').indexOf(notStartedItem.get_item('Title')) > -1) {
                  alreadyScheduled = true;
                  break;
              }
          }
          if (alreadyScheduled === false) {
    
                // SP.ListItemCreationInformation holds the information the SharePoint server needs to
                // create a list item
              var calendarItem = new SP.ListItemCreationInformation();
    
                // The some_list .additem method tells the server which list to add
                // the item to.
              var itemToCreate = calendarList.addItem(calendarItem);
    
                // The some_item .set_item method sets the value of the specified field.
              itemToCreate.set_item('Title', 'Orient ' + notStartedItem.get_item('Title'));
    
                // The EventDate and EndDate are the start and stop times of an event.
              itemToCreate.set_item('EventDate', '2015-06-' + dayOfMonth + 'T21:00:00Z');
              itemToCreate.set_item('EndDate', '2015-06-' + dayOfMonth + 'T23:00:00Z');
              dayOfMonth++;
    
                // The update method tells the server to commit the changes to the SharePoint database.
              itemToCreate.update();
              unscheduledItems = true;
          }
      }
      if (unscheduledItems) {
          calendarList.update();
          clientContext.executeQueryAsync(onScheduleItemsSuccess, onScheduleItemsFail);
      }
    }
    

    关于此代码,请注意以下几点:

    • 此方法检查“西雅图新员工”列表中“未启动”项的称呼(即员工姓名)是否包含在“员工入职培训计划”日历的事件标题中。 这是以简单化假设为依据,即日历中的所有项在创建时均在事件标题中添加了员工全名。

    • 如果日历中已有的事件与"未启动"项目均不匹配,脚本将为该"未启动"项目创建一个日历项。

    • JSOM 使用轻型 ListItemCreationInformation 对象,而不是 SPListItem 对象,以最大限度地减小发送到 SharePoint 服务器的有效负载大小。

    • 新日历事件的两个日期/时间字段设置为本文撰写时相应月份 (2015-06) 内的几号。 将这些日期更改为当前年份和月份内的几号,这样就无需在日历中向后滚动查找项了。

    • 如果发现任何"未启动"项目尚未计划,请将第一个"未启动"项目计划为该月的 10 日。 每个增加的未计划项目计划为后一天。 我们为简化做出了一个假设,即不会有很多未计划项目,因此不会产生诸如"32"一样不可能的日期。

    • 此代码大多是 JavaScript。 对于使用 SharePoint JSOM 的行提供了注释。

  8. 添加以下成功处理程序,当向日历中添加以前未计划的项目时,将会调用此处理程序。

    function onScheduleItemsSuccess() {
      alert('There was one or more unscheduled orientations and they have been added to the '
                + 'Employee Orientation Schedule calendar.');
    }
    
  9. 将以下失败函数添加到该文件的失败回调部分。

      function onGetNotStartedItemsFail(sender, args) {
        alert('Unable to get the not-started items. Error:'
            + args.get_message() + '\n' + args.get_stackTrace());
    }
    
    function onGetScheduledItemsFail(sender, args) {
        alert('Unable to get scheduled items from host web. Error:'
            + args.get_message() + '\n' + args.get_stackTrace());
    }
    
    function onScheduleItemsFail(sender, args) {
        alert('Unable to schedule items on host web calendar. Error:'
            + args.get_message() + '\n' + args.get_stackTrace());
    }
    
  10. 打开 default.aspx 文件,并查找 ID 为“PlaceHolderMain”的“asp:Content”元素。

  11. purgeCompletedItems 按钮正下方添加以下标记。

    <p><asp:Button runat="server" OnClientClick="return ensureOrientationScheduling()"
                   ID="ensureorientationschedulingbutton"
                   Text="Ensure all items are on the Calendar" /></p>
    
  12. 在 Visual Studio 中重新生成项目。

  13. 为了在测试加载项时尽量不用手动将列表项的“入职培训阶段”设置为“未启动”,请打开列表实例“NewEmployeesInSeattle”的 elements.xml 文件(而不是列表模板“NewEmployeeOrientation”的 elements.xml),并确保至少有三个“Row”元素(包括 Cassie Hicks 所在的行)的“入职培训阶段”值为“未启动”。 由于这是默认值,因此最简单的方法是确保这三行或更多行的 OrientationStage 没有“Field”元素。

    下面的示例展示了“Row”元素。

    <Rows>
      <Row>
        <Field Name="Title">Tom Higginbotham</Field>
        <Field Name="Division">Manufacturing</Field>
        <Field Name="OrientationStage">Completed</Field>
      </Row>
      <Row>
        <Field Name="Title">Satomi Hayakawa</Field>
      </Row>
      <Row>
        <Field Name="Title">Cassi Hicks</Field>
      </Row>
      <Row>
        <Field Name="Title">Lertchai Treetawatchaiwong</Field>
      </Row>
    </Rows>
    

指定外接程序所需的主机 Web 的权限

外接程序自动拥有对其自身外接程序 Web 的完整控制权限,因此到目前为止,您还不需要指定其需要的权限。 但是您必须定期请求对与之交换数据的主机 Web 的权限。 "员工定向"外接程序需具有相应权限才能向主机 Web 中的日历添加项目。

  1. 在"解决方案资源管理器"中,打开 appmanifest.xml 文件。

  2. 在清单设计器中,打开“权限”选项卡。

  3. 在“范围”列的最上面一行中,从下拉列表中选择“列表”

  4. 在“权限”列中,选择“管理”

  5. 如果“属性”列保留为空白,加载项会请求获取对主机 Web 上每个列表的写入权限。 最佳做法是将加载项限制为仅获取所需的权限。 虽然无法在加载项清单中将加载项限制为仅获取对特定列表实例的权限,但可以限制为仅获取对在特定基列表模板基础之上生成的列表实例的权限。 日历的基列表模板是数字 ID 为 106 的“事件”

    选择同一行的“属性”单元格,这样就可以在此单元格中看到“编辑”按钮。 此时,权限列表应如下所示。

    图 2. 带有可见“编辑”按钮的权限列表

    Visual Studio 加载项清单设计器中“权限”选项卡上的权限列表,其中“属性”列单元格中显示“编辑”按钮。

  6. 选择“编辑”,打开“属性”对话框。

  7. 将“名称”设置为“BaseTemplateId”,再将“值”设置为“106”。 此时,对话框应如下所示。

    图 3. 列表权限属性对话框

    Visual Studio 中列表权限的“属性”对话框,其中属性名设置为“基列表 ID”,值设置为“106”。

  8. 选择“确定”。 此时,“权限”选项卡应如下所示。

    图 4. Visual Studio 中外接程序清单设计器的“权限”选项卡

    Visual Studio 中外接程序清单设计器的“权限”选项卡,显示外接程序希望具有管理基类型 106 的列表的权限。

运行并测试加载项

  1. 请确保已按照本文前面所述准备好主机 Web 日历。 日历中应包含一个事件“Cassi Hicks 入职培训”

  2. 在调试时 Visual Studio 使用的浏览器中启用弹出窗口。

  3. 按 F5 键部署并运行加载项。 Visual Studio 在测试 SharePoint 网站上临时安装此外接程序并立即运行。

  4. 权限许可表单打开,可以在其中授予加载项所需的权限。 在此页的下拉列表中,可以从主机 Web 上的所有日历中进行选择。 依次选择“员工入职培训计划”和“信任它”

    图 5. SharePoint 外接程序同意提示

    SharePoint 加载项许可提示,其中简要说明了加载项需要的权限,并包含“信任它”或“取消”按钮。

  5. 在加载项的起始页完全加载后,选择“确保项已经过计划”按钮。

    图 6. 带有新按钮的“员工定向”主页

    “员工入职培训”主页中新增了标签为“确保项已经过计划”的按钮。

  6. 如果运行任何出错回调函数,将会看到回调函数创建的错误消息警报。 否则,就会看到最终成功回调创建的成功消息:已将一个或多个未计划的入职培训添加到“员工入职培训计划”日历中

  7. 转到主机 Web 上的“员工入职培训计划”日历。 例如,依次选择指向开发人员网站主页的痕迹导航链接和“网站内容”。 选择“员工入职培训计划”磁贴(而不是“员工入职培训”磁贴)。

    日历看起来应该如下所示。 脚本检测到已有 Cassi Hicks 的事件,因此不会再创建一个。 它为其他两个定向处于"未启动"状态的员工创建事件。 它不会为"未启动"阶段已经过去的员工创建事件。

    图 7. 添加两个新事件后的日历

    “员工入职培训计划”日历中添加了在当月 10 号和 11 号为两名员工提供入职培训的新事件

  8. 确保先从日历中删除这两个新事件,再重新选择“确保项已经过计划”

  9. 若要结束调试会话,请关闭浏览器窗口或停止在 Visual Studio 中进行调试。 每次按 F5,Visual Studio 都会撤回旧版加载项并安装最新版本。

  10. 将在其他文章中使用此加载项和 Visual Studio 解决方案,因此最好在使用一段时间后,再最后撤回一次加载项。 在“解决方案资源管理器”中,右键单击此项目,再选择“撤回”

后续步骤

转到 SharePoint 托管的 SharePoint 外接程序中的高级工作: