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 日


