Sharepoint AddIn 使用 Angular 4 和 WebPack
用于显示 SP 列表数据的 SharePoint Add-In(Angular 4)。使用 NPM、WebPack、TypeScript、SP CSOM、Rxjs 和 ag-Grid
引言
可以采用多种方式访问、操作和更新 SharePoint 数据。目前,新的 SharePoint Framework (SPFx) 是重点,但您也可以通过依赖 SharePoint REST API 的现代技术来操作 SharePoint 数据。
本文的 Add-In 可以从 SharePoint Online(我将使用它,请注意)或 SharePoint on premise 访问数据。数据来自 SharePoint 列表,并且可以检测到数据的变化。数据通过 ag-Grid
控件和 Rxjs Observables 进行显示。
本文将介绍如何使用 Angular4、Typescript 和 NPM 访问 SharePoint 列表数据。重点是如何创建、设置并将其打包到一个项目中,而不是教授如何制作 SharePoint Add-In、Angular 或 JS 编程。
因此,将其视为一个蓝图,可以轻松地修改成使用 Vue 或 React SPA 而不是 Angular。
背景
从 SharePoint 2010 WebPart 开发转过来,适应快速发展的、与我编写的服务器端代码以及仅用于加速的轻量级 JS/JQuery 助手截然不同的前端开发,花了一些时间。
因此,目标是尝试将所有当前前端的优秀工具打包到一个 SharePoint Add-In 中。尽管有几篇关于这个主题的文章,但没有一篇是完整的,当我尝试“正确地”打包 Angular 应用时,我不得不深入研究。这里的“正确地”指的是使用 aspx 页面作为登陆页面,不手动复制/编辑项目中的文件等等。
我从这两篇文章中学到了不少技巧,所以也请看看它们
设置
有一些环境设置,让我们开始吧。
Visual Studio
我假设您已经安装了 Visual Studio 2017。我使用的是 Professional 版本,但您也可以尝试 Community 版本。必需安装 Office 模板才能创建 SharePoint Add-In 项目。
您需要 Visual Studio 的 NPM Task Runner Tool。您可以在 VS Marketplace 上获取。稍后我们将使用此工具来自动化整个构建过程。
NPM + Webpack
前端开发者喜欢频繁更换工具和框架,有时很难跟上,至少对我来说是这样。为了确保 Add-In 的 Angular4 部分可以被编译并正确打包,我使用了 Webpack 模块打包器,它为您处理了所有的繁重工作。
要同时使用 Angular 和 Webpack,您需要安装 NPM (Node Package Manager)。您可以从 Node JS 官方网站下载。
安装 NPM 后,就可以安装 Webpack 了。您可以从控制台运行 npm
命令,也可以通过 VS 包管理器控制台进行操作。
要下载并安装 Webpack,请运行此命令
npm install webpack --save-dev
就是这样!
SharePoint
要运行此代码,您需要一个完全配置好的 SharePoint 场,或者可以使用 Office 365 环境。如果您没有访问权限,或者懒得向管理员申请开发站点集,您可以随时获得一个月的 Office 免费 365 试用环境。
搭建项目基础
该动手了。首先,创建我们将用作基础的 SharePoint Add-In 项目。
第一个对话框要求您输入开发站点集的 URL 以及将要使用的托管模型。对于这个项目,我使用 SharePoint 托管。之后的屏幕会询问您要部署到哪个 SharePoint 版本。如果您使用本地部署,请选择您使用的版本。如前所述,我将使用 SharePoint Online。
项目“优化”
当我提到优化时,我想的是删除我们不需要的文件。对吧?
首先要删除的是默认添加的 JQuery NuGet。在 NuGet 包管理器中,选择 JQuery 并单击“卸载”按钮。
接下来,我们将添加一个新的 SharePoint 模块并命名为“app
”(右键单击项目 -> 添加 -> 新建项 -> Office/Sharepoint -> 模块)。删除新创建模块中的 Sample.txt。
在项目 根目录
下创建两个新文件夹:config 和 src。在 src 文件夹中,创建两个新文件夹:app 和 assets。在 assets 文件夹中,创建 css 和 image 文件夹。
我们还需要删除 SharePoint Add-In 模板创建的现有模块。
- 将 App.css(是的,它是空的)文件从 Content 模块移动到 src/assets/css,并将其重命名为 styles.css。然后删除 Content 模块。
- 将 AppIcon.png 从 Image 模块移动到 app 模块。编辑清单文件,使用浏览按钮设置新图标位置。然后删除 Image 模块。
- 将 Default.aspx 从 Pages 模块移动到 src 文件夹。然后删除
Pages
模块。 - 删除
Scripts
模块
完成这些步骤后,您的项目应该如下所示
创建 Angular SPA
根据您对 Angular 的熟练程度,您可以使用 Angular CLI 的 ng new 命令,然后将 src 文件夹复制到 项目
中的 src 文件夹。安装 Angular CLI 可以加快创建新组件的速度。
您可以使用 Visual Studio(添加 -> 新建项 -> 在搜索中键入“npm configuration
”)或使用命令行提示符中的 npm
,它会询问一些关于项目的问题并创建文件。
npm init
无论哪种方式,您都需要向项目中添加 package.json 文件。我们的项目配置需要列出该项目将使用的所有内容,因此请编辑 package.json 并确保其如下所示
{
"name": "sp-angular4-addin",
"version": "1.0.0",
"description": "Angular observable grid for Sharepoint list",
"scripts": {
"build": "webpack --config config/webpack.dev.js --progress --profile --bail"
},
"author": "Nemanja Sarovic",
"license": "ISC",
"dependencies": {
"@angular/animations": "~4.0.3",
"@angular/common": "~4.0.3",
"@angular/compiler": "~4.0.3",
"@angular/core": "~4.0.3",
"@angular/forms": "~4.0.3",
"@angular/http": "~4.0.3",
"@angular/platform-browser": "~4.0.3",
"@angular/platform-browser-dynamic": "~4.0.3",
"@angular/router": "~4.0.3",
"ag-grid": "^16.0.1",
"ag-grid-angular": "^16.0.0",
"camljs": "2.6.2",
"core-js": "^2.4.1",
"zone.js": "~0.8.5"
},
"devDependencies": {
"@angular/cli": "^1.6.7",
"@types/core-js": "^0.9.41",
"@types/node": "^6.0.45",
"@types/sharepoint": "2016.1.0",
"angular2-template-loader": "^0.6.2",
"awesome-typescript-loader": "^3.4.1",
"css-loader": "^0.26.1",
"extract-text-webpack-plugin": "^2.0.0-beta.5",
"file-loader": "^0.9.0",
"handlebars": "^4.0.11",
"handlebars-loader": "^1.6.0",
"html-loader": "^0.4.3",
"html-webpack-plugin": "^2.30.1",
"lodash": "^4.17.5",
"node-sass": "^4.7.2",
"null-loader": "^0.1.1",
"raw-loader": "^0.5.1",
"rxjs": "^5.0.2",
"sass-loader": "6.0.6",
"style-loader": "^0.13.1",
"typescript": "2.4.0",
"webpack": "^2.2.1",
"webpack-dev-server": "2.4.1",
"webpack-merge": "^3.0.0"
}
}
配置 TypeScript
我们需要在 src 文件夹中创建 tsconfig.json 文件。这将告诉 TypeScript 如何将 ts 文件编译为 js。
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [
"es2016",
"dom"
],
"noImplicitAny": false,
"suppressImplicitAnyIndexErrors": true,
"types": [
"node",
"core-js",
"sharepoint"
],
"typeRoots": [
"../node_modules/@types"
]
}
}
配置 WebPack
我稍后会解释一些 webpack 配置,这里我们将创建 config 文件。
在项目 根目录
下,创建 webpack.config.js。此文件应只有一行,指向实际的配置文件。
module.exports = require('./config/webpack.dev.js');
在 config 文件夹中,我们需要创建另外三个文件,以确保 WebPack 能够打包必要的文件。
首先,创建 helpers.js 文件。
var path = require('path');
var _root = path.resolve(__dirname, '..');
function root(args) {
args = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [_root].concat(args));
}
exports.root = root;
创建 webpack.common.js。入口点定义了 webpack 将在其中打包文件的 js 文件,而模块定义了 webpack 在解析项目文件时将使用的转换工具。
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var helpers = require('./helpers');
module.exports = {
entry: {
'polyfills': './src/polyfills.ts',
'vendor': './src/vendor.ts',
'app': './src/main.ts'
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
loaders: [
{
loader: 'awesome-typescript-loader',
options: { configFileName: helpers.root('src', 'tsconfig.json') }
}, 'angular2-template-loader'
]
},
{ test: /\.html$/, loader: 'html-loader' },
{
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
loader: 'file-loader?name=assets/[name].[hash].[ext]'
},
{
test: /\.css$/,
exclude: helpers.root('src', 'app'),
loader: ExtractTextPlugin.extract
({ fallbackLoader: 'style-loader', loader: 'css-loader?sourceMap' })
},
{
test: /\.scss$/,
loaders: ["style-loader", "css-loader", "sass-loader"]
},
{
test: /\.css$/,
include: helpers.root('src', 'app'),
loader: 'raw-loader'
},
{
test: /\.hbs$/,
loader: 'handlebars-loader'
}
]
},
plugins: [
new webpack.ContextReplacementPlugin(
/angular(\\|\/)core(\\|\/)@angular/,
helpers.root('./src'), // location of your src
{} // a map of your routes
),
new webpack.optimize.CommonsChunkPlugin({
name: ['app', 'vendor', 'polyfills']
}),
new HtmlWebpackPlugin({
filename: 'Default.aspx',
template: '!!handlebars-loader!src/Default.aspx',
inject: false
})
]
};
最后,创建 webpack.dev.js。
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');
module.exports = webpackMerge(commonConfig, {
devtool: 'source-map',
output: {
path: helpers.root('app'),
publicPath: '',
filename: '[name].js',
chunkFilename: '[id].chunk.js'
},
plugins: [
new ExtractTextPlugin('[name].css')
],
devServer: {
historyApiFallback: true,
stats: 'minimal'
}
});
配置任务运行器
在任务运行器资源管理器(Ctrl+Alt+Bkspce)中,应该会列出您的 Add-In。右键单击左侧窗格中的 build 命令,然后选择 Bindings/Before Build。
这样,我们将确保在 VS 开始 Add-In 的 .NET 代码编译和构建过程之前,Webpack 可以启动 TypeScript 编译、打包和最小化。
开始编写代码!
在此之前,让我们运行 npm
来获取 package.js 中列出的所有必需 js 文件。在项目文件夹中打开 cmd
并运行
npm install
当 npm
完成文件下载后,让我们创建第一个 TypeScript 文件。
好的,现在是时候编写实际代码了。:)
在 src 文件夹中,创建 main.ts。它是 Angular SPA 的入口点,除了导入必需的模块外,它还会调用名为 AppModule
的模块。
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { AppModule } from './app/app.module';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/map';
if (process.env.ENV === 'production') {
enableProdMode();
}
document.addEventListener('DOMContentLoaded', _ => {
platformBrowserDynamic().bootstrapModule(AppModule)
});
在 src 文件夹中,创建 polyfills.ts。Polyfills 确保所有浏览器提供您所需的相同级别的支持。
import 'core-js/es6';
import 'core-js/es7/reflect';
require('zone.js/dist/zone');
if (process.env.ENV === 'production') {
// Production
} else {
// Development and test
Error['stackTraceLimit'] = Infinity;
require('zone.js/dist/long-stack-trace-zone');
}
在同一个 src 文件夹中,创建 vendor.ts。此文件导入您需要的 js 资源。如您所见,我们正在使用 Angular 框架、用于 observable 模式的 RxJS 和用于网格数据显示的 ag-Grid
样式。
Webpack 将识别我们链接的 CSS 文件,并将它们打包到 vendor.css 中,这样我们就可以在 Add-In 中使用这些样式,而不必担心我们的 SharePoint 模块是否引用了这些文件,是否将其放入了 Style
库以及在旧式 SharePoint WebPart 部署中需要克服的所有这些障碍。很方便。
// Angular
import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/core';
import '@angular/common';
import '@angular/http';
import '@angular/router';
// RxJS
import 'rxjs';
// ag-Grid
import '../node_modules/ag-grid/dist/styles/ag-grid.css'
import '../node_modules/ag-grid/dist/styles/ag-theme-fresh.css';
如您所见,main.ts 引导了 AppModule
模块。让我们定义该模块 - 在 /src/app 文件夹中创建 app.module
。
在 AppModule
中,我们导入了 SpListModule
,其中包含执行所有工作的 SpListComponent
组件。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { HttpModule } from '@angular/http'
import { FormsModule } from '@angular/forms';
import { APP_BASE_HREF } from '@angular/common';
import { SpListComponent } from './components/splist/splist.component';
import { SpListModule } from './components/splist.module';
import { AgGridModule } from "ag-grid-angular/main";
@NgModule({
imports: [
BrowserModule,
HttpModule,
CommonModule,
FormsModule,
SpListModule,
AgGridModule.withComponents([])
],
exports: [
],
declarations: [
],
providers: [{
provide: APP_BASE_HREF,
useValue: '/'
}],
bootstrap: [SpListComponent]
})
export class AppModule { }
现在,在 /src/app 文件夹中创建一个 components 文件夹。在该文件夹中,再创建一个名为 splist 的文件夹。我们将在其中创建 Angular 组件。您可以使用 Angular CLI 来创建组件,它会创建四个文件,包含实际的组件代码、组件 HTML、组件样式和组件测试。我将手动进行,如您所见。
在 /src/app/components/splist 文件夹中,创建 splist.component.html 文件。此标记创建 agGrid 组件,它将为我们处理所有繁重的工作。
<div style="width: 800px;">
<ag-grid-angular #agGrid style="width: 100%; height: 350px;"
class="ag-theme-fresh"
[gridOptions]="gridOptions"
[columnDefs]="columnDefs"
[rowData]="rowData">
</ag-grid-angular>
</div>
在同一个文件夹中,创建 splist.component.ts。
此代码定义了网格列,并使用 MyListItem
定义从 MyListService
加载数据。所有操作都在构造函数中完成,在构造函数中加载数据以供初始显示,然后每分钟从服务轮询一次。为了使 agGrid
能够区分记录,请设置 RowNodeId
值。
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { GridOptions } from "ag-grid/main";
import { MyListService } from '../../shared/services/MyListService'
import { MyListItem } from '../../shared/models/MyListItem';
@Component({
selector: 'my-app',
templateUrl: './splist.component.html',
providers: [MyListService]
})
export class SpListComponent {
gridOptions: GridOptions;
private rowData: MyListItem[];
initialRowData$;
rowDataUpdates$;
constructor(myService: MyListService) {
this.rowDataUpdates$ = myService.getItemsChanged();
this.initialRowData$ = myService.getItems();
this.gridOptions = <GridOptions>{
enableRangeSelection: true,
enableColResize: true,
columnDefs: this.createColumnDefs(),
getRowNodeId: function (data) {
return data.Id;
},
onGridReady: () => {
this.initialRowData$.subscribe((initial) => {
if (this.gridOptions.api) {
this.gridOptions.api.setRowData(initial);
}
});
this.rowDataUpdates$.subscribe((updates) => {
if (this.gridOptions.api) {
console.log("update " + JSON.stringify(updates));
this.gridOptions.api.updateRowData({ update: updates })
}
});
this.gridOptions.api.sizeColumnsToFit();
}
};
}
private createColumnDefs() {
return [
{ headerName: "ID", field: "Id", width: 70 },
{ headerName: "Title", field: "Title", width: 280 },
{
headerName: "Amount", field: "Amount", width: 100,
cellClass: 'cell-number',
valueFormatter: this.numberFormatter,
cellRenderer: 'animateShowChange'
},
{ headerName: "Urgent", field: "Urgent", width: 280 }
]
}
numberFormatter(params) {
if (typeof params.value === 'number') {
return params.value.toFixed(2);
} else {
return params.value;
}
}
}
好的。表示部分完成了。现在,让我们创建模型和服务。
在 /src/app 文件夹中创建 shared 文件夹。在该文件夹中,创建 models 和 services 文件夹。
在 /src/app/shared/models 文件夹内,创建 MyListItem.ts。这是我们定义与 SPList
结构对应的模型的地方(我们还没有创建它,稍后会讲到)。它是一个简单的类,因为我们的演示列表也很简单。
export class MyListItem {
public Id: number;
public Title: string;
public Amount: number;
public Urgent: string;
}
现在到服务部分。在 /src/app/shared/services 文件夹中创建 MyListService.ts。这是从 SharePoint 拉取实际数据的地方。我使用的是 CSOM,并决定不通过包含 project.json 中的 camljs 来使事情复杂化,以防我决定提供分页数据等。
与常规 CSOM 一样,您必须通过执行 SP.ClientContext
来获取上下文。getItems
和 getItemsChanged
之间的唯一区别在于使用 RxJS Observables,它通过 interval 将轮询时间设置为 60 秒。
我只是展示了如何监控 SP 列表,但在现实中,如此频繁地轮询 SharePoint 并不是最佳实践。在实际场景中,我会使用更多逻辑来防止对 SP 场造成不必要的负载。最好的方法可能是使用 SP.LastItemModifiedDate
。
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { MyListItem } from '../models/MyListItem';
import { cloneDeep } from 'lodash'
export class MyListService {
private spHostUrl: string;
private clientContext: SP.ClientContext;
private appContext: SP.AppContextSite;
private listName = "Shopping List";
private listContext: SP.List;
constructor() {
SP.SOD.executeFunc('sp.js', 'SP.ClientContext', () => {
this.spHostUrl = GetUrlKeyValue('SPHostUrl');
this.clientContext = SP.ClientContext.get_current();
this.appContext = new SP.AppContextSite(this.clientContext, this.spHostUrl);
});
}
public getItems(): Observable<MyListItem[]> {
let self = this;
let caml = SP.CamlQuery.createAllItemsQuery();
let agItems: MyListItem[] = [];
this.listContext = this.appContext.get_web().get_lists().getByTitle(this.listName);
let listItems = self.listContext.getItems(caml);
self.clientContext.load(listItems);
return Observable.create((observer) => {
self.clientContext.executeQueryAsync(
() => {
for (let index = 0; index < listItems.get_count(); index++) {
let item = listItems.get_item(index);
let fieldValues = item.get_fieldValues();
agItems.push({
Id: fieldValues.ID,
Title: fieldValues.Title,
Amount: fieldValues.Amount,
Urgent: fieldValues.Urgent
});
}
},
(sender: any, args: SP.ClientRequestFailedEventArgs) => {
console.log("Service error: ", args);
}
);
});
}
public getItemsChanged(): Observable<MyListItem[]> {
let self = this;
let caml = SP.CamlQuery.createAllItemsQuery();
let agItems: MyListItem[] = [];
this.listContext = this.appContext.get_web().get_lists().getByTitle(this.listName);
let listItems = self.listContext.getItems(caml);
self.clientContext.load(listItems);
return Observable.create((observer) => {
const interval = setInterval(() => {
agItems = [];
let cnt = 0;
self.clientContext.executeQueryAsync(
() => {
for (let index = 0; index < listItems.get_count(); index++) {
let item = listItems.get_item(index);
let fieldValues = item.get_fieldValues();
agItems.push({
Id: fieldValues.ID,
Title: fieldValues.Title,
Amount: fieldValues.Amount,
Urgent: fieldValues.Urgent
});
}
},
(sender: any, args: SP.ClientRequestFailedEventArgs) => {
console.log("Service error: ", args);
}
);
observer.next(cloneDeep(agItems));
}, 20000);
return () => clearInterval(interval);
});
}
}
Rxjs 提供了一种优雅的方式来处理异步数据检索,即使使用 interval 和 observables 看起来很笨拙。当然,我也可以使用 promises 和 async/await 从 SharePoint 获取数据,然后在 ngInit
中初始化轮询数据的 interval。
ngOnInit() {
this.myInterval = setInterval(() => {
myService.getItems();
}, 60000);
}
ngOnDestroy() {
clearInterval(myInterval);
}
}
当然,使用 promises 的代码应该略有不同。创建 promise 后,我将使用 async/await 来获取数据。
public async getItemsSync() {
let items: MyListItem[] = [];
try {
items = await this.getItemsPromised();
} catch (error) {
console.log("getItemSync error");
}
return items;
}
protected getItemsPromised(): Promise<MyListItem[]> {
let self = this;
let spItems: MyListItem[] = [];
let caml = SP.CamlQuery.createAllItemsQuery();
this.listContext = this.appContext.get_web().get_lists().getByTitle(this.listName);
let listItems = self.listContext.getItems(caml);
self.clientContext.load(listItems);
let promise = new Promise<MyListItem[]>((resolve, reject) => {
self.clientContext.executeQueryAsync(() => {
for (let index = 0; index < listItems.get_count(); index++) {
let item = listItems.get_item(index);
let fieldValues = item.get_fieldValues();
spItems.push({
Id: fieldValues.ID,
Title: fieldValues.Title,
Amount: fieldValues.Amount,
Urgent: fieldValues.Urgent
});
}
resolve(spItems);
},
(sender: any, args: SP.ClientRequestFailedEventArgs) => {
console.log("Service error: ", args);
reject();
}
);
});
return promise;
}
您最终应该得到一个类似这样的结构
SharePoint 列表
最后,我们需要创建我们要监控的列表。这应该足够简单了?列表结构如下图所示。
我们需要确保我们的 Add-In 拥有访问 SPWeb 资源的足够权限。编辑 AppManifest.xml,然后在 Permissions 选项卡上,将 Scope 设置为 Web,将 Permissions 设置为 Read。如果您计划从网格中编辑 SPList
数据,您需要将该权限提升到 Write
,这在本地 SharePoint 权限中相当于 Contributor
。
Webpack 来拯救
好的。一切都准备就绪了。我们有了 Angular 代码,有了网格控件,有了检索数据的服务。很棒。
问题是,当 ts 代码被编译和打包时,您需要在 SharePoint Add-In 入口页面中包含所有必需的 js 和 css 文件。这应该不是大问题,但问题在于,默认情况下,Angular 使用下划线解析器在 HTML 中执行注入。当然,aspx 页面有很多 <% %>
ASP.NET 标签,您需要配置下划线使用不同的标签。或者您可以使用不同的加载器。
我使用了 handlebars(自从我第一次发现它们以来我就很喜欢它),并且由于 WebPack 非常灵活,我加载了 handlebars-loader 并使用它来解析 aspx 页面并执行注入。另一个问题是,标准的注入模板会在文件(加载器注入链接的位置)中插入 <head>
标签,如果它缺失的话,所以我不得不制作自己的模板并完全绕过 WebPack 的开箱即用功能。幸运的是,所有东西都易于定制(尽管您需要深入研究大量文档才能了解如何做到这一点),我们得到了全新的 SharePoint 页面模板。
如果您查看我们一开始创建的 WebPack,您会在 plugins 部分注意到以下代码
new HtmlWebpackPlugin({
filename: 'Default.aspx',
template: '!!handlebars-loader!src/Default.aspx',
inject: false
})
这就是 WebPack 使用 handlebars-loader
并使用它来处理 src/Default.aspx 文件的位置。还记得 Default.aspx 文件吗?就是我们从 Pages 模块复制过来的那个?好吧,让我们修改它,以便 WebPack 和 handlebars-loader
能够理解它。
Microsoft 的页面模板说明了在哪里插入 CSS 和在哪里插入 js 链接。我们需要使用 handlebars 标记和 WebPack 数据来插入它们。
所以,将 aspx 页面中 asp:Content tag PlaceHolderAdditionalPageHead
下的原始部分替换为如下所示的标记
<!-- Add your CSS styles to the following file -->
<link rel="Stylesheet" type="text/css" href="../Content/App.css" />
<!-- Add your JavaScript to the following file -->
<script type="text/javascript" src="../Scripts/App.js"></script>
用此标记替换它。您会注意到 js 标签具有 defer
属性,以确保延迟加载并允许 SharePoint 脚本加载。我们可以使用 SP.SOD
调用,但在此方式下我没有遇到任何问题。
<!-- Add your CSS styles to the following file -->
{{#each htmlWebpackPlugin.files.css}}
<link href="{{this}}" rel="stylesheet">
{{/each}}
<!-- Add your JavaScript to the following file -->
{{#each htmlWebpackPlugin.files.chunks}}
<script type="text/javascript" src="{{this.entry}}" defer></script>
{{/each}}
另外,从内容标签的顶部删除以下行,否则您会收到缺少 jQuery 库的警告。
<script type="text/javascript" src="../Scripts/jquery-1.9.1.min.js"></script>
最终打包
最后,我们需要将所有内容整齐地打包到 SharePoint 模块中。选择项目中的“显示所有文件”,然后将所有文件包含到项目中(右键单击,包含到项目中)。
这样就可以了。右键单击项目并部署它。不要忘记检查 AppManifest.xml,确保 app/Default.aspx 是您的起始页,并确保图标链接到我们项目开始时移动的 .ico 文件。
SharePoint Online 应该会询问您是否信任 Add-In,之后,您将获得带有 agGrid 的 Angular4 AddIn,它使用 Observables 来显示列表数据。
待办事项
- 编辑/更新列表数据
- 使列表轮询的负载变轻
历史
- 1.0 - 初始版本,2018 年 3 月 1 日