生成 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 部件迁移到了 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 带来了许多好处。 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,可简化代码并维护解决方案。