生成 SharePoint 解决方案时,SharePoint 开发人员经常使用 FullCalendar jQuery 插件,在日历视图中显示数据。 FullCalendar 是标准 SharePoint 日历视图的绝佳替代,因为它可以将任务呈现为多个日历列表中的日历数据、非日历列表中的数据或 SharePoint 外部的数据。 本文介绍了如何将使用 FullCalendar 且由脚本编辑器 Web 部件生成的 SharePoint 自定义迁移到 SharePoint 框架。

显示为使用脚本编辑器 Web 部件生成的日历的任务列表

为了说明将使用 FullCalendar 的 SharePoint 自定义迁移到 SharePoint 框架的过程,将使用以下解决方案,其中概览了从 SharePoint 列表中检索到的任务的日历视图。

该解决方案是使用标准 SharePoint 脚本编辑器 Web 部件生成的。 下面是自定义项使用的代码。

<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.js"></script>
<link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css" />
<div id="calendar"></div>
<script>
  var PATH_TO_DISPFORM = _spPageContextInfo.webAbsoluteUrl + "/Lists/Tasks/DispForm.aspx";
  var TASK_LIST = "Tasks";
  var COLORS = ['#466365', '#B49A67', '#93B7BE', '#E07A5F', '#849483', '#084C61', '#DB3A34'];
  displayTasks();
  function displayTasks() {
    $('#calendar').fullCalendar('destroy');
    $('#calendar').fullCalendar({
      weekends: false,
      header: {
        left: 'prev,next today',
        center: 'title',
        right: 'month,basicWeek,basicDay'
      displayEventTime: false,
      // open up the display form when a user clicks on an event
      eventClick: function (calEvent, jsEvent, view) {
        window.location = PATH_TO_DISPFORM + "?ID=" + calEvent.id;
      editable: true,
      timezone: "UTC",
      droppable: true, // this allows things to be dropped onto the calendar
      // update the end date when a user drags and drops an event
      eventDrop: function (event, delta, revertFunc) {
        updateTask(event.id, event.start, event.end);
      // put the events on the calendar
      events: function (start, end, timezone, callback) {
        var startDate = start.format('YYYY-MM-DD');
        var endDate = end.format('YYYY-MM-DD');
        var restQuery = "/_api/Web/Lists/GetByTitle('" + TASK_LIST + "')/items?$select=ID,Title,\
Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
$filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";
        $.ajax({
          url: _spPageContextInfo.webAbsoluteUrl + restQuery,
          type: "GET",
          dataType: "json",
          headers: {
            Accept: "application/json;odata=nometadata"
          .done(function (data, textStatus, jqXHR) {
            var personColors = {};
            var colorNo = 0;
            var events = data.value.map(function (task) {
              var assignedTo = task.AssignedTo.map(function (person) {
                return person.Title;
              }).join(', ');
              var color = personColors[assignedTo];
              if (!color) {
                color = COLORS[colorNo++];
                personColors[assignedTo] = color;
              if (colorNo >= COLORS.length) {
                colorNo = 0;
              return {
                title: task.Title + " - " + assignedTo,
                id: task.ID,
                color: color, // specify the background color and border color can also create a class and use className parameter.
                start: moment.utc(task.StartDate).add("1", "days"),
                end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day
            callback(events);
  function updateTask(id, startDate, dueDate) {
    // subtract the previously added day to the date to store correct date
    var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
      startDate.format("hh:mm") + ":00Z";
    if (!dueDate) {
      dueDate = startDate;
    var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
      dueDate.format("hh:mm") + ":00Z";
    $.ajax({
      url: _spPageContextInfo.webAbsoluteUrl + '/_api/contextinfo',
      type: 'POST',
      headers: {
        'Accept': 'application/json;odata=nometadata'
      .then(function (data, textStatus, jqXHR) {
        return $.ajax({
          url: _spPageContextInfo.webAbsoluteUrl +
          "/_api/Web/Lists/getByTitle('" + TASK_LIST + "')/Items(" + id + ")",
          type: 'POST',
          data: JSON.stringify({
            StartDate: sDate,
            DueDate: dDate,
          headers: {
            Accept: "application/json;odata=nometadata",
            "Content-Type": "application/json;odata=nometadata",
            "X-RequestDigest": data.FormDigestValue,
            "IF-MATCH": "*",
            "X-Http-Method": "PATCH"
      .done(function (data, textStatus, jqXHR) {
        alert("Update Successful");
      .fail(function (jqXHR, textStatus, errorThrown) {
        alert("Update Failed");
      .always(function () {
        displayTasks();
</script>

此解决方案以 Mark Rackley(Office 服务器和服务 MVP 以及 PAIT Group 首席战略官)的工作成果为依据。 若要详细了解原始解决方案,请参阅使用 FullCalendar.io 在 SharePoint 中创建自定义日历

首先,在前 <script><link> 元素中自定义加载它使用的库:jQuery、Moment.js 和 FullCalendar。

接下来,它定义向其中注入生成的日历视图的 <div>

然后,它定义两个函数,即用于在日历视图中显示任务的 displayTasks(),以及在将任务拖放到不同日期后触发,且更新基础列表项上日期的 updateTask()。 每个函数都定义自己的 REST 查询,可用于与 SharePoint 列表 REST API 通信,从而检索或更新列表项。

使用 FullCalendar jQuery 插件,用户可以轻松获得丰富的解决方案。此类解决方案可以使用不同的颜色标记各个事件,也可以使用拖放操作重新整理事件等。

将任务日历解决方案从脚本编辑器 Web 部件迁移到 SharePoint 框架

将基于脚本编辑器 Web 部件的自定义转换为 SharePoint 框架带来了许多好处,如解决方案配置更用户友好、解决方案管理更集中化。 下面逐步介绍了如何将解决方案迁移到 SharePoint 框架。

首先,将解决方案迁移到 SharePoint 框架,并尽量不要更改原始代码。 随后,将解决方案的代码转换为 TypeScript,以便受益于它的开发时类型安全性功能。同时,将部分代码替换为 SharePoint 框架 API,以充分受益于它的功能,并进一步简化解决方案。

有关不同迁移阶段的项目源代码,请参阅教程:将使用脚本编辑器 Web 部件生成的 jQuery 和 FullCalendar 解决方案迁移到 SharePoint 框架

新建 SharePoint 框架项目

  • 首先,为项目新建文件夹:

    md fullcalendar-taskscalendar
    
  • 导航到项目文件夹:

    cd fullcalendar-taskscalendar
    
  • 在项目文件夹中,运行 SharePoint Framework Yeoman 生成器,以搭建新的 SharePoint Framework 项目:

    yo @microsoft/sharepoint
    
  • 出现提示时,请输入以下值(为下面省略的所有提示选择默认选项):

  • 解决方案的名称是什么?: fullcalendar-taskscalendar
  • 你想要为你的组件设定哪些基准包?:仅 SharePoint Online (最新)
  • 要创建哪种类型的客户端组件?:Web 部件
  • Web 部件的名称是什么?:Tasks calendar
  • Web 部件的说明是什么?:在日历视图中显示任务
  • 要使用哪种框架?:没有 JavaScript 框架
  • 在代码编辑器中,打开项目文件夹。 在本教程中,你将使用 Visual Studio Code。

    加载 JavaScript 库

    类似于使用脚本编辑器 Web 部件生成的原始解决方案,首先需要加载解决方案所需的 JavaScript 库。 在 SharePoint 框架中,这通常分为两步,即第一步指定加载的库 URL,第二步在代码中引用库。

  • 指定要加载的库 URL。

    在代码编辑器中,打开 ./config/config.json 文件,并将 externals 部分更改为:

    // .. "externals": { "jquery": { "path": "https://code.jquery.com/jquery-1.11.1.min.js", "globalName": "jQuery" "fullcalendar": { "path": "https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.js", "globalName": "jQuery", "globalDependencies": [ "jquery" "moment": "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js" // ..
  • 打开 ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts 文件,并在最后一个 import 语句后面添加:

    import 'jquery';
    import 'moment';
    import 'fullcalendar';
    

    定义容器 div

    和在原始解决方案中一样,下一步是定义应呈现日历的位置。

    在代码编辑器中,打开 ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts 文件,并将 render() 方法更改为:

    export default class ItRequestsWebPart extends BaseClientSideWebPart<IItRequestsWebPartProps> {
      public render(): void {
        this.domElement.innerHTML = `
          <div class="${styles.tasksCalendar}">
            <link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css" />
            <div id="calendar"></div>
          </div>`;
      // ...
    

    启动 FullCalendar 并加载数据

    最后一步是添加代码,以启动 FullCalendar jQuery 插件,并从 SharePoint 加载数据。

  • 在“./src/webparts/tasksCalendar”文件夹中,新建“script.js”文件,并粘贴以下代码:

    var moment = require('moment');
    var PATH_TO_DISPFORM = window.webAbsoluteUrl + "/Lists/Tasks/DispForm.aspx";
    var TASK_LIST = "Tasks";
    var COLORS = ['#466365', '#B49A67', '#93B7BE', '#E07A5F', '#849483', '#084C61', '#DB3A34'];
    displayTasks();
    function displayTasks() {
      $('#calendar').fullCalendar('destroy');
      $('#calendar').fullCalendar({
        weekends: false,
        header: {
          left: 'prev,next today',
          center: 'title',
          right: 'month,basicWeek,basicDay'
        displayEventTime: false,
        // open up the display form when a user clicks on an event
        eventClick: function (calEvent, jsEvent, view) {
          window.location = PATH_TO_DISPFORM + "?ID=" + calEvent.id;
        editable: true,
        timezone: "UTC",
        droppable: true, // this allows things to be dropped onto the calendar
        // update the end date when a user drags and drops an event
        eventDrop: function (event, delta, revertFunc) {
          updateTask(event.id, event.start, event.end);
        // put the events on the calendar
        events: function (start, end, timezone, callback) {
          var startDate = start.format('YYYY-MM-DD');
          var endDate = end.format('YYYY-MM-DD');
          var restQuery = "/_api/Web/Lists/GetByTitle('" + TASK_LIST + "')/items?$select=ID,Title,\
              Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
              $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";
          $.ajax({
            url: window.webAbsoluteUrl + restQuery,
            type: "GET",
            dataType: "json",
            headers: {
              Accept: "application/json;odata=nometadata"
            .done(function (data, textStatus, jqXHR) {
              var personColors = {};
              var colorNo = 0;
              var events = data.value.map(function (task) {
                var assignedTo = task.AssignedTo.map(function (person) {
                  return person.Title;
                }).join(', ');
                var color = personColors[assignedTo];
                if (!color) {
                  color = COLORS[colorNo++];
                  personColors[assignedTo] = color;
                if (colorNo >= COLORS.length) {
                  colorNo = 0;
                return {
                  title: task.Title + " - " + assignedTo,
                  id: task.ID,
                  color: color, // specify the background color and border color can also create a class and use className parameter.
                  start: moment.utc(task.StartDate).add("1", "days"),
                  end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day
              callback(events);
    function updateTask(id, startDate, dueDate) {
      // subtract the previously added day to the date to store correct date
      var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
        startDate.format("hh:mm") + ":00Z";
      if (!dueDate) {
        dueDate = startDate;
      var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
        dueDate.format("hh:mm") + ":00Z";
      $.ajax({
        url: window.webAbsoluteUrl + '/_api/contextinfo',
        type: 'POST',
        headers: {
          'Accept': 'application/json;odata=nometadata'
        .then(function (data, textStatus, jqXHR) {
          return $.ajax({
            url: window.webAbsoluteUrl +
            "/_api/Web/Lists/getByTitle('" + TASK_LIST + "')/Items(" + id + ")",
            type: 'POST',
            data: JSON.stringify({
              StartDate: sDate,
              DueDate: dDate,
            headers: {
              Accept: "application/json;odata=nometadata",
              "Content-Type": "application/json;odata=nometadata",
              "X-RequestDigest": data.FormDigestValue,
              "IF-MATCH": "*",
              "X-Http-Method": "PATCH"
        .done(function (data, textStatus, jqXHR) {
          alert("Update Successful");
        .fail(function (jqXHR, textStatus, errorThrown) {
          alert("Update Failed");
        .always(function () {
          displayTasks();
    

    此代码与脚本编辑器 Web 部件自定义的原始代码几乎完全相同。 唯一区别是,原始代码从 SharePoint 设置的全局 _spPageContextInfo 变量检索到了当前网站的 URL,而 SharePoint 框架中的代码则使用必须在 Web 部件中设置的自定义变量。

    SharePoint 框架客户端 Web 部件既可用于经典页面,也可用于新式页面。 虽然经典页面上有 _spPageContextInfo 变量,但新式页面上没有,这就是为什么不能依赖它,而需要使用可以自行控制的自定义属性的原因所在。

  • 为了能够在 Web 部件中引用此文件,在代码编辑器中打开 ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts 文件,并将 render() 方法更改为:

    export default class ItRequestsWebPart extends BaseClientSideWebPart<IItRequestsWebPartProps> {
      public render(): void {
        this.domElement.innerHTML = `
          <div class="${styles.tasksCalendar}">
            <link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css" />
            <div id="calendar"></div>
          </div>`;
        (window as any).webAbsoluteUrl = this.context.pageContext.web.absoluteUrl;
        require('./script');
      // ...
    
  • 在命令行中运行以下命令,验证 Web 部件是否按预期正常运行:

    gulp serve --nobrowser
    

    由于 Web 部件是从 SharePoint 加载数据,因此必须使用托管的 SharePoint 框架 Workbench 测试 Web 部件。

  • 导航到 https://{your-tenant-name}.sharepoint.com/_layouts/workbench.aspx,并将 Web 部件添加到画布。 此时,应该会看到借助 FullCalendar jQuery 插件在日历视图中显示的任务。

    添加支持以便通过 Web 部件属性配置 Web 部件

    在前面的步骤中,已将任务日历解决方案从脚本编辑器 Web 部件迁移到了 SharePoint 框架。 虽然解决方案已按预期正常运行,但未受益于 SharePoint 框架的任何优势。 从中加载任务的列表的名称包含在代码中,而代码本身是纯 JavaScript,与 TypeScript 相比,更难重构。

    下面逐步介绍了如何扩展现有解决方案,以便用户能够指定从中加载数据的列表的名称。 稍后,将代码转换为 TypeScript,以便受益于它的类型安全性功能。

    定义 Web 部件属性以便存储列表名称

  • 定义 Web 部件属性,用于存储应从中加载任务的列表的名称。 在代码编辑器中,打开 ./src/webparts/tasksCalendar/TasksCalendarWebPart.manifest.json 文件,将默认的 description 属性重命名为 listName,并清除它的值。

  • 将 Web 部件属性接口更新为反映清单中的更改。 在代码编辑器中,打开“./src/webparts/tasksCalendar/ITasksCalendarWebPartProps.ts”文件,并将它的内容更改为:

    export interface ITasksCalendarWebPartProps {
      listName: string;
    
  • 更新 listName 属性的显示标签。

    打开“./src/webparts/tasksCalendar/loc/mystrings.d.ts”文件,并将它的内容更改为:

    declare interface ITasksCalendarStrings {
      PropertyPaneDescription: string;
      BasicGroupName: string;
      ListNameFieldLabel: string;
    declare module 'tasksCalendarStrings' {
      const strings: ITasksCalendarStrings;
      export = strings;
    
  • 打开“./src/webparts/tasksCalendar/loc/en-us.js”文件,并将它的内容更改为:

    define([], function() {
      return {
        "PropertyPaneDescription": "Tasks calendar settings",
        "BasicGroupName": "Data",
        "ListNameFieldLabel": "List name"
    
  • 将 Web 部件更新为使用新定义的属性。 在代码编辑器中,打开“./src/webparts/tasksCalendar/TasksCalendarWebPart.ts”文件,并将“getPropertyPaneConfiguration”方法更改为:

    export default class TasksCalendarWebPart extends BaseClientSideWebPart<ITasksCalendarWebPartProps> {
      // ...
      protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
        return {
          pages: [
              header: {
                description: strings.PropertyPaneDescription
              groups: [
                  groupName: strings.BasicGroupName,
                  groupFields: [
                    PropertyPaneTextField('listName', {
                      label: strings.ListNameFieldLabel
      protected get disableReactivePropertyChanges(): boolean {
        return true;
    

    为了防止 Web 部件在用户键入列表名称时重新加载,还将 Web 部件配置为使用非反应属性窗格,具体操作是添加 disableReactivePropertyChanges() 方法,并将它的返回值设置为 true

    使用已配置的列表名称从中加载数据

    最初,应从中加载数据的列表的名称嵌入到了 REST 查询中。 既然用户现在可以配置此名称,应先将已配置的值注入 REST 查询,再执行它们。 这样做的最简单方法是,将 script.js 文件的内容移到主 Web 部件文件。

  • 在代码编辑器中,打开“./src/webparts/tasksCalendar/TasksCalendarWebPart.ts”文件。

  • 找到先前添加到文件的以下代码:

    import 'jquery';
    import 'moment';
    import 'fullcalendar';
    

    将代码更新为以下代码:

    var $: any = require('jquery');
    var moment: any = require('moment');
    import 'fullcalendar';
    var COLORS = ['#466365', '#B49A67', '#93B7BE', '#E07A5F', '#849483', '#084C61', '#DB3A34'];
    

    由于 Moment.js 是在后面要使用的代码中引用,因此 TypeScript 必须知道它的名称,否则将无法生成项目。 这同样适用于 jQuery。

    由于 FullCalendar 是能够将自身附加到 jQuery 对象的 jQuery 插件,因此不需要更改其导入方式。

    最后一部分包括复制颜色列表,以用于标记不同的事件。

  • script.js 文件中复制 displayTasks()updateTask() 函数,并在 TasksCalendarWebPart 类中将他们粘贴为以下内容:

    export default class TasksCalendarWebPart extends BaseClientSideWebPart<ITasksCalendarWebPartProps> {
      // ...
      private displayTasks() {
        $('#calendar').fullCalendar('destroy');
        $('#calendar').fullCalendar({
          weekends: false,
          header: {
            left: 'prev,next today',
            center: 'title',
            right: 'month,basicWeek,basicDay'
          displayEventTime: false,
          // open up the display form when a user clicks on an event
          eventClick: (calEvent, jsEvent, view) => {
            (window as any).location = this.context.pageContext.web.absoluteUrl +
              "/Lists/" + escape(this.properties.listName) + "/DispForm.aspx?ID=" + calEvent.id;
          editable: true,
          timezone: "UTC",
          droppable: true, // this allows things to be dropped onto the calendar
          // update the end date when a user drags and drops an event
          eventDrop: (event, delta, revertFunc) => {
            this.updateTask(event.id, event.start, event.end);
          // put the events on the calendar
          events: (start, end, timezone, callback) => {
            var startDate = start.format('YYYY-MM-DD');
            var endDate = end.format('YYYY-MM-DD');
            var restQuery = "/_api/Web/Lists/GetByTitle('" + escape(this.properties.listName) + "')/items?$select=ID,Title,\
    Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
    $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";
            $.ajax({
              url: this.context.pageContext.web.absoluteUrl + restQuery,
              type: "GET",
              dataType: "json",
              headers: {
                Accept: "application/json;odata=nometadata"
              .done((data, textStatus, jqXHR) => {
                var personColors = {};
                var colorNo = 0;
                var events = data.value.map((task) => {
                  var assignedTo = task.AssignedTo.map((person) => {
                    return person.Title;
                  }).join(', ');
                  var color = personColors[assignedTo];
                  if (!color) {
                    color = COLORS[colorNo++];
                    personColors[assignedTo] = color;
                  if (colorNo >= COLORS.length) {
                    colorNo = 0;
                  return {
                    title: task.Title + " - " + assignedTo,
                    id: task.ID,
                    color: color, // specify the background color and border color can also create a class and use className parameter.
                    start: moment.utc(task.StartDate).add("1", "days"),
                    end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day
                callback(events);
      private updateTask(id, startDate, dueDate) {
        // subtract the previously added day to the date to store correct date
        var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
          startDate.format("hh:mm") + ":00Z";
        if (!dueDate) {
          dueDate = startDate;
        var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
          dueDate.format("hh:mm") + ":00Z";
        $.ajax({
          url: this.context.pageContext.web.absoluteUrl + '/_api/contextinfo',
          type: 'POST',
          headers: {
            'Accept': 'application/json;odata=nometadata'
          .then((data, textStatus, jqXHR) => {
            return $.ajax({
              url: this.context.pageContext.web.absoluteUrl +
              "/_api/Web/Lists/getByTitle('" + escape(this.properties.listName) + "')/Items(" + id + ")",
              type: 'POST',
              data: JSON.stringify({
                StartDate: sDate,
                DueDate: dDate,
              headers: {
                Accept: "application/json;odata=nometadata",
                "Content-Type": "application/json;odata=nometadata",
                "X-RequestDigest": data.FormDigestValue,
                "IF-MATCH": "*",
                "X-Http-Method": "PATCH"
          .done((data, textStatus, jqXHR) => {
            alert("Update Successful");
          .fail((jqXHR, textStatus, errorThrown) => {
            alert("Update Failed");
          .always(() => {
            this.displayTasks();
      // ...
    

    与之前的情况相比,代码有了一些变化。

  • 通过将 function 关键字替换为 private 修饰符,纯 JavaScript 函数现已更改为 TypeScript 方法。 为了将它们添加到 TaskCalendarWebPart 类,这是必需的。
  • 因为两种方法现在与 Web 部件位于同一个文件,因此可以使用 this.context.pageContext.web.absoluteUrl 属性直接从 Web 部件上下文访问它,而无需定义全局变量来包含当前网站的 URL。
  • 在所有 REST 查询中,固定列表名称还替换为 listName 属性值,其中包含用户配置的列表名称。 使用此值前,它要使用 lodash 的 escape() 函数进行转义,以禁止脚本注入。
  • 最后一步,更改 render() 方法以调用新添加的 displayTasks() 方法:

    export default class TasksCalendarWebPart extends BaseClientSideWebPart<ITasksCalendarWebPartProps> {
      public render(): void {
        this.domElement.innerHTML = `
          <div class="${styles.tasksCalendar}">
            <link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css" />
            <div id="calendar"></div>
          </div>`;
        this.displayTasks();
      // ...
    
  • 由于你将 script.js 文件的内容移到了主 Web 部件文件中,因此不再需要 script.js 文件,可以将它从项目中删除。

  • 若要验证 Web 部件是否按预期正常运行,请在命令行中运行以下命令:

    gulp serve --nobrowser
    
  • 转到托管的 Workbench,并将 Web 部件添加到画布中。 打开 Web 部件属性窗格,指定包含任务的列表的名称,再选择“应用”按钮以确认更改。 此时,应该会看到 Web 部件的日历视图中显示有任务。

    将纯 JavaScript 代码转换为 TypeScript

    与纯 JavaScript 相比,使用 TypeScript 带来了许多好处。 TypeScript 不仅更易于维护和重构,还能够尽早捕获错误。 下面逐步介绍了如何将原始 JavaScript 代码转换为 TypeScript。

    添加已使用库的类型声明

    为了正常运行,TypeScript 会要求项目使用的不同库的类型声明。 类型声明通常在命名空间中 @types 以 npm 包的形式分发。

  • 在命令行中运行以下命令,安装 jQuery 的类型声明:

    npm install @types/jquery@1 --save-dev
    

    Moment.js 的类型声明与 Moment.js 包一起分发。 尽管要通过 URL 加载 Moment.js 来使用它的类型定义,但仍需要在项目中安装 Moment.js 包。

  • 在命令行中运行以下命令,安装 Moment.js 包:

    npm install moment --save
    

    你会注意到,我们不会安装 FullCalendar 库的类型声明。 本文提出的解决方案使用的是旧版本的 FullCalendar,该版本在那时还没有有效的类型声明,因此我们只能选择性地在我们的解决方案中使用 TypeScript 类型。

    理想情况下,可考虑将项目升级到 FullCalendar 库的新版本。 那样超出了本文的范畴,因为自我们初学者解决方案所基于的版本以来,已经发生了大量的 API 更改。

    更新包引用

    若要使用已安装类型声明中的类型,必须更改库的引用方式。

  • 在代码编辑器中,打开 ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts 文件

  • 找到先前添加到文件的以下代码:

    var $: any = require('jquery');
    var moment: any = require('moment');
    import 'fullcalendar';
    

    将代码更新为以下代码:

    import * as $ from 'jquery';
    import 'fullcalendar';
    import * as moment from 'moment';
    

    将主 Web 部件文件更新为 TypeScript

    至此,已添加项目中安装的所有库的类型声明,可以开始将纯 JavaScript 代码转换为 TypeScript 了。

  • 定义一个接口,用于从 SharePoint 列表检索到的任务。 在代码编辑器中,打开“./src/webparts/tasksCalendar/TasksCalendarWebPart.ts”文件,并在 Web 部件类的正上方,添加以下代码片段:

    interface ITask {
      ID: number;
      Title: string;
      StartDate: string;
      DueDate: string;
      AssignedTo: [{ Title: string }];
    
  • 在 web 部件类中,将 displayTasks()updateTask() 方法更改为:

    private displayTasks() {
      ($('#calendar') as any).fullCalendar('destroy');
      ($('#calendar') as any).fullCalendar({
        weekends: false,
        header: {
          left: 'prev,next today',
          center: 'title',
          right: 'month,basicWeek,basicDay'
        displayEventTime: false,
        // open up the display form when a user clicks on an event
        eventClick: (calEvent: any, jsEvent: MouseEvent, view: any): void => {
          (window as any).location = this.context.pageContext.web.absoluteUrl +
            "/Lists/" + escape(this.properties.listName) + "/DispForm.aspx?ID=" + calEvent.id;
        editable: true,
        timezone: "UTC",
        droppable: true, // this allows things to be dropped onto the calendar
        // update the end date when a user drags and drops an event
        eventDrop: (event: any, delta: moment.Duration, revertFunc: Function): void => {
          this.updateTask(<number>event.id, <moment.Moment>event.start, <moment.Moment>event.end);
        // put the events on the calendar
        events: (start: moment.Moment, end: moment.Moment, timezone: string, callback: Function): void => {
          var startDate = start.format('YYYY-MM-DD');
          var endDate = end.format('YYYY-MM-DD');
          var restQuery = "/_api/Web/Lists/GetByTitle('" + escape(this.properties.listName) + "')/items?$select=ID,Title,\
      Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
      $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";
          $.ajax({
            url: this.context.pageContext.web.absoluteUrl + restQuery,
            type: "GET",
            dataType: "json",
            headers: {
              Accept: "application/json;odata=nometadata"
            .done((data: { value: ITask[] }, textStatus: string, jqXHR: JQueryXHR) => {
              let personColors: { [person: string]: string; } = {};
              var colorNo = 0;
              var events = data.value.map((task: ITask): any => {
                var assignedTo = task.AssignedTo.map((person: { Title: string }): string => {
                  return person.Title;
                }).join(', ');
                var color = personColors[assignedTo];
                if (!color) {
                  color = COLORS[colorNo++];
                  personColors[assignedTo] = color;
                if (colorNo >= COLORS.length) {
                  colorNo = 0;
                return {
                  title: task.Title + " - " + assignedTo,
                  id: task.ID,
                  color: color, // specify the background color and border color can also create a class and use className parameter.
                  start: moment.utc(task.StartDate).add("1", "days"),
                  end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day
              callback(events);
    private updateTask(id: number, startDate: moment.Moment, dueDate: moment.Moment) {
      // subtract the previously added day to the date to store correct date
      var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
        startDate.format("hh:mm") + ":00Z";
      if (!dueDate) {
        dueDate = startDate;
      var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
        dueDate.format("hh:mm") + ":00Z";
      $.ajax({
        url: this.context.pageContext.web.absoluteUrl + '/_api/contextinfo',
        type: 'POST',
        headers: {
          'Accept': 'application/json;odata=nometadata'
        .then((data: { FormDigestValue: string }, textStatus: string, jqXHR: JQueryXHR): JQueryXHR => {
          return $.ajax({
            url: this.context.pageContext.web.absoluteUrl +
              "/_api/Web/Lists/getByTitle('" + escape(this.properties.listName) + "')/Items(" + id + ")",
            type: 'POST',
            data: JSON.stringify({
              StartDate: sDate,
              DueDate: dDate,
            headers: {
              Accept: "application/json;odata=nometadata",
              "Content-Type": "application/json;odata=nometadata",
              "X-RequestDigest": data.FormDigestValue,
              "IF-MATCH": "*",
              "X-Http-Method": "PATCH"
        .done((data: {}, textStatus: string, jqXHR: JQueryXHR): void => {
          alert("Update Successful");
        .fail((jqXHR: JQueryXHR, textStatus: string, errorThrown: string) => {
          alert("Update Failed");
        .always((): void => {
          this.displayTasks();
      // ...
    

    如果将纯 JavaScript 转换为 TypeScript,首先注意到的变化是显式类型。 虽然它们不是必需的,但它们明确指明了预期的数据类型。 TypeScript 可立即捕获任何不遵守指定协定的行为,有助于开发人员在开发过程中发现潜在问题。 处理 AJAX 响应及其数据时,这是非常有用的。

    可能已注意到的另一个变化是,TypeScript 字符串内插。 使用字符串内插,不仅可以简化动态字符串的构成,还能提升代码可读性。

    比较纯 JavaScript:

    var restQuery = "/_api/Web/Lists/GetByTitle('" + TASK_LIST + "')/items?$select=ID,Title,\
    Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
    $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";
    
    const restQuery: string = `/_api/Web/Lists/GetByTitle('${escape(this.properties.listName)}')/items?$select=ID,Title,\
    Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
    $filter=((DueDate ge '${startDate}' and DueDate le '${endDate}')or(StartDate ge '${startDate}' and StartDate le '${endDate}'))`;
    

    使用 TypeScript 字符串内插的其他好处是,无需转义引号,这也简化了 REST 查询的构成。

  • 若要确认一切是否按预期正常运行,请在命令行中运行以下命令:

    gulp serve --nobrowser
    
  • 转到托管的 Workbench,并将 Web 部件添加到画布中。 尽管看上去没有变化,但新基准代码使用的是 TypeScript 及其类型声明,以帮助用户维护解决方案。

    将 jQuery AJAX 调用替换为 SharePoint 框架 API

    该解决方案当前使用 jQuery AJAX 调用来与 SharePoint REST API 进行通信。 对于常规 GET 请求,jQuery AJAX API 与使用 SharePoint 框架 SPHttpClient API 一样方便。 实际差别在于执行 POST 请求(如更新事件那样)时:

    $.ajax({
      url: this.context.pageContext.web.absoluteUrl + '/_api/contextinfo',
      type: 'POST',
      headers: { 'Accept': 'application/json;odata=nometadata'}
      .then((data: { FormDigestValue: string }, textStatus: string, jqXHR: JQueryXHR): JQueryXHR => {
        return $.ajax({
          url: `${this.context.pageContext.web.absoluteUrl}\
    /_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`,
          type: 'POST',
          data: JSON.stringify({
            StartDate: sDate,
            DueDate: dDate,
          headers: {
            Accept: "application/json;odata=nometadata",
            "Content-Type": "application/json;odata=nometadata",
            "X-RequestDigest": data.FormDigestValue,
            "IF-MATCH": "*",
            "X-Http-Method": "PATCH"
      .done((data: {}, textStatus: string, jqXHR: JQueryXHR): void => {
        alert("Update Successful");
      // ...
    

    由于要更新列表项,因此需要向 SharePoint 提供有效的请求摘要令牌。 虽然摘要可用于经典页面,但它仅在 3 分钟内有效。 因此,在执行更新操作之前,亲自检索有效令牌总是最安全的。 获取请求摘要后,必须将它添加到更新请求的请求头。 否则,请求会失败。

    SharePoint 框架 SPHttpClient API 简化了与 SharePoint 的通信,因为它可以自动检测请求是否是 POST 请求,以及是否需要有效的请求摘要。 如果需要,SPHttpClient API 会自动从 SharePoint 检索它,并将它添加到请求中。 相比之下,使用 SPHttpClient API 发出的相同请求如下所示:

    this.context.spHttpClient.post(`${this.context.pageContext.web.absoluteUrl}\
    /_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`, SPHttpClient.configurations.v1, {
      body: JSON.stringify({
        StartDate: sDate,
        DueDate: dDate,
      headers: {
        Accept: "application/json;odata=nometadata",
        "Content-Type": "application/json;odata=nometadata",
        "IF-MATCH": "*",
        "X-Http-Method": "PATCH"
    .then((response: SPHttpClientResponse): void => {
      // ...
    
  • 若要将原始的 jQuery AJAX 调用替换为 SharePoint 框架 SPHttpClient API,在代码编辑器中打打开 ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts 文件。 向现有 import 语句列表添加以下内容:

    import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
    
  • TasksCalendarWebPart 类中,使用以下代码替换 displayTasks()updateTask() 方法:

    private displayTasks() {
      ($('#calendar') as any).fullCalendar('destroy');
      ($('#calendar') as any).fullCalendar({
        weekends: false,
        header: {
          left: 'prev,next today',
          center: 'title',
          right: 'month,basicWeek,basicDay'
        displayEventTime: false,
        // open up the display form when a user clicks on an event
        eventClick: (calEvent: any, jsEvent: MouseEvent, view: any): void => {
          (window as any).location = this.context.pageContext.web.absoluteUrl +
            "/Lists/" + escape(this.properties.listName) + "/DispForm.aspx?ID=" + calEvent.id;
        editable: true,
        timezone: "UTC",
        droppable: true, // this allows things to be dropped onto the calendar
        // update the end date when a user drags and drops an event
        eventDrop: (event: any, delta: moment.Duration, revertFunc: Function): void => {
          this.updateTask(<number>event.id, <moment.Moment>event.start, <moment.Moment>event.end);
        // put the events on the calendar
        events: (start: moment.Moment, end: moment.Moment, timezone: string, callback: Function): void => {
          var startDate = start.format('YYYY-MM-DD');
          var endDate = end.format('YYYY-MM-DD');
          var restQuery = "/_api/Web/Lists/GetByTitle('" + escape(this.properties.listName) + "')/items?$select=ID,Title,\
      Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
      $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";
          this.context.spHttpClient.get(this.context.pageContext.web.absoluteUrl + restQuery, SPHttpClient.configurations.v1, {
            headers: {
              'Accept': "application/json;odata.metadata=none"
            .then((response: SPHttpClientResponse): Promise<{ value: ITask[] }> => {
              return response.json();
            .then((data: { value: ITask[] }): void => {
              let personColors: { [person: string]: string; } = {};
              var colorNo = 0;
              var events = data.value.map((task: ITask): any => {
                var assignedTo = task.AssignedTo.map((person: { Title: string }): string => {
                  return person.Title;
                }).join(', ');
                var color = personColors[assignedTo];
                if (!color) {
                  color = COLORS[colorNo++];
                  personColors[assignedTo] = color;
                if (colorNo >= COLORS.length) {
                  colorNo = 0;
                return {
                  title: task.Title + " - " + assignedTo,
                  id: task.ID,
                  color: color, // specify the background color and border color can also create a class and use className parameter.
                  start: moment.utc(task.StartDate).add("1", "days"),
                  end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day
              callback(events);
    private updateTask(id: number, startDate: moment.Moment, dueDate: moment.Moment) {
      // subtract the previously added day to the date to store correct date
      var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
        startDate.format("hh:mm") + ":00Z";
      if (!dueDate) {
        dueDate = startDate;
      var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
        dueDate.format("hh:mm") + ":00Z";
      this.context.spHttpClient.post(`${this.context.pageContext.web.absoluteUrl}\
    /_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`, SPHttpClient.configurations.v1, {
        body: JSON.stringify({
          StartDate: sDate,
          DueDate: dDate,
        headers: {
          Accept: "application/json;odata=nometadata",
          "Content-Type": "application/json;odata=nometadata",
          "IF-MATCH": "*",
          "X-Http-Method": "PATCH"
        .then((response: SPHttpClientResponse): void => {
          if (response.ok) {
            alert("Update Successful");
          } else {
            alert("Update Failed");
          this.displayTasks();
    

    若要禁止 SharePoint REST API 响应中的元数据,必须在使用 SharePoint 框架 SPHttpClient 时,确保将 application/json;odata.metadata=none(而不是application/json;odata=nometadata )用作 Accept 头的值。 SPHttpClient 使用 OData 4.0,并且需要第一个值。 如果改用后者,请求会失败,并显示“406 不可接受”响应。

  • 若要确认一切是否按预期正常运行,请在命令行中运行以下命令:

    gulp serve --nobrowser
    
  • 转到托管的 Workbench,并将 Web 部件添加到画布中。 尽管看上去仍没有变化,但新代码使用的是 SharePoint 框架 SPHttpClient,可简化代码并维护解决方案。

  •