65.9K
CodeProject 正在变化。 阅读更多。
Home

Sharepoint AddIn 使用 Angular 4 和 WebPack

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2018 年 3 月 7 日

CPOL

11分钟阅读

viewsIcon

26084

用于显示 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

在项目 根目录 下创建两个新文件夹:configsrc。在 src 文件夹中,创建两个新文件夹:appassets。在 assets 文件夹中,创建 cssimage 文件夹。

我们还需要删除 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 文件夹。在该文件夹中,创建 modelsservices 文件夹。

/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 来获取上下文。getItemsgetItemsChanged 之间的唯一区别在于使用 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 日
© . All rights reserved.