2016年10月我参加了在北京举行的DevDays Asia 2016 - Office 365应用开发”48小时黑客马拉松“,我开发的一个Word Add-In Demo——WordTemplateHelper得到了二等奖。在会场有幸结识了陈希章老师,在与陈老师的交流中受益良多,得知陈老师在准备一个Office解决方案系列后,我想把这个Demo的开发过程简要介绍给你们,以支持陈老师的无私奉献,也但愿更多的开发者参与到Office365的开发中来。css
Office相关开发主要能够参考这个地址:https://dev.office.com/getting-startedhtml
本篇文章主要介绍其中的Office加载项开发,即Office Add-ins:https://msdn.microsoft.com/ZH-CN/library/office/jj220082.aspx前端
什么是Office Add-ins呢?在陈老师的上一篇文章中,对整个Office发展历史都进行了梳理,我我的的理解就是,开发者能够在Office提供的平台上,对Office作出必定的扩展以实现各类功能,好比以前录制的宏,写的VBS的脚本,某种意义上均可以看作是Office的Add-ins。固然这只是我的理解,不必定准确。目前的Office Add-Ins只支持Office2013之后的版本,开发方式也和之前的VBS有了很大的区别。git
如今的Office Add-ins结构是这样的:github
一个Office Add-in实际上是一个Web App,能够将其部署在任意位置,它能够在一个Office应用程序中运行。有一个manifest.xml清单文件用来指定该Web App如何来呈现,包括定义Web App 的URL。当Office加载这个Add-in时,其实是提供了一个浏览器的环境,来运行指定的Web App。也就是说,如今开发一个Office Add-in,其实跟开发网页程序差很少,这对熟悉html+JavaScript+css的前端开发人员是很是容易上手的。微软提供了丰富的JavaScript API来对Office进行操做,能实现什么就取决于开发者的想象力了。typescript
一个Word Add-In的实例:数据库
我在得知有这个活动时,并无想好要作什么,一直到坐上赴京的高铁,才慢慢有了一个想法,这个想法也是来自平时的工做须要。在工做中常常要撰写大量的文档,如各类软件需求规格说明书、公函、文书、操做手册等,这些文档都有规定的格式,通常状况下我是将一些已经写好的Word文档保存在一个文件夹里当作模板,下次写这种文档的时候复制一份,删删减减的再改。为什么不本身写个程序,将这些具备固定模式的文档做为Word模板呢?虽然Word也有本身的模板,但其实是很是有限的,并不能彻底知足咱们的须要。若是这个功能作成一个模板商店,你们能够自由上传、分享各自的模板,也许会方便许多。npm
Word自带的模板是这样的:json
这些通用模板对专业性比较强的工做来讲是远远不够的。Word Template Helper的效果是这样的:后端
主意有了,那么就来看一下如何实现。我参加活动时的项目托管在码云上,为了写这篇文章,我从新梳理了这个小demo,在Github上建了一个项目,并尝试使用最新的.NET Core来实现后台API部分。接下来就跟我一块儿动手吧。
首先分析一下该项目的结构。文档的模板数据,如模板标题、属性等,须要保存在数据库里,还须要一个Web API项目提供数据,Office Add-in为一个纯前端项目,使用Angular2框架,采用异步调用Web API的数据,实现搜索、加载模板等功能。插件的UI使用微软提供的Fabric UI。整个项目的技术栈以下所示:
至于文档的实体——Word文档,是以Word格式文件存储仍是直接保存在数据库中呢?若是是正式项目的话,固然是保存在云存储中是最合适的,但对于一个sample来讲,直接保存在数据库中也何尝不可。由于是参加开发马拉松,怎么快怎么来吧。包括ORM框架也是,只是为了快速实现采用的方式,不是最佳实践。
这个sample的开发环境配置以下:
Windows 10 x64,
VS 2017(请确保安装了Office开发工具)
VS Code
Node.js v7.10.0
NPM v4.2.0
ASP.NET Core 1.1
VS2017已经正式发布了,我使用最新的.NET Core来实现Web API层。
新建一个空白解决方案,命名为WordTemplateHelpe,而后在其中添加一个ASP.NET Core项目:
选择Web API:
在nuget管理器中搜索安装一下几个Nuget包:
Microsoft.EntityFrameworkCore.SqlServer:EF Core SQL Server
Microsoft.EntityFrameworkCore.Tools:EF命令行工具
Microsoft.EntityFrameworkCore.Tools.DotNet:EF Core命令行工具
目前最新的EF都推荐使用Code First模式,即直接写Model,EF框架会自动建立所需的数据库。若是习惯DB First的话,也有一个很好的工具推荐:EntityFramework-Reverse-POCO-Code-First-Generator:https://visualstudiogallery.msdn.microsoft.com/ee4fcff9-0c4c-4179-afd9-7a2fb90f5838
能够直接在VS的扩展与更新里下载。这个工具能够很方便的根据数据库生成所需的实体类。
首先添加一个模板类型的枚举:
/// <summary> /// 类型 /// </summary> public enum TemplateType { /// <summary> /// Private /// </summary> [Description("Private")] Private = 0, /// <summary> /// Public /// </summary> [Description("Public")] Public = 1, /// <summary> /// Organization /// </summary> [Description("Organization")] Organization = 2, }
添加一个模板类:
public class PrivateTemplateInfo { ///<summary> /// Id ///</summary> public string Id { get; set; } ///<summary> /// User Id ///</summary> public string UserId { get; set; } ///<summary> /// Template Id ///</summary> public string TemplateId { get; set; } ///<summary> /// Create Time ///</summary> public DateTime CreateTime { get; set; } }
由于还须要组织机构模板、用户收藏等几个表,这里就不写了,可参考Github上的示例。
有了Model后,须要指定哪些实体包含在数据模型中。添加一个Data文件夹,在其中建立一个名为WordTemplateContext.cs的文件:
public class WordTemplateContext:DbContext { public WordTemplateContext(DbContextOptions<WordTemplateContext> options) : base(options) { } public DbSet<WordTemplateInfo> WordTemplateInfoes { get; set; } public DbSet<UserFavoriteInfo> UserFavoriteInfoes { get; set; } public DbSet<PrivateTemplateInfo> PrivateTemplateInfoes { get; set; } public DbSet<OrganizationTemplateInfo> OrganizationTemplateInfoes { get; set; } }
这样就为每一个实体建立了一个DbSet,对应数据库中的表,实体对应表中的行。
ASP.NET Core默认实现了依赖注入。要把刚才创建的WordTemplateContext注册成服务,须要在Startup.cs中添加如下代码:
public void ConfigureServices(IServiceCollection services) { // Add framework services. services.AddDbContext<WordTemplateContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddMvc(); }
注意要添加using Microsoft.EntityFrameworkCore;否则会找不到UseSqlServer方法。
数据库链接字符串在appsettings.json中配置:
{ "ConnectionStrings": { "DefaultConnection": "Server=.;User ID=sa;Password=12QWasZX;Initial Catalog=WordTemplate;" }, "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Warning" } } }
这里使用了LocalDb,用于测试。当须要正式部署时,这里须要更改成正式数据库服务器的地址及用户名密码。
下面使用命令行初始化数据库。在Data目录下新建一个DbInitializer类,输入如下方法:
public static class DbInitializer { public static void Initialize(WordTemplateContext context) { context.Database.EnsureCreated(); //TODO context.SaveChanges(); } }
确保数据被建立。而后修改Startup.cs文件中的Configure方法:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, WordTemplateContext context) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); app.UseMvc(); DbInitializer.Initialize(context); }
如今写个Controller看看。在Controller文件夹中添加一个控制器:
这里可使用依赖注入,将数据库上下文注入进来:
[Produces("application/json")] [Route("api/WordTemplate/[action]")] public class WordTemplateController : Controller { private readonly WordTemplateContext _context; public WordTemplateController(WordTemplateContext context) { _context = context; }
咱们以一个搜索模板的api为例:
[HttpGet] public async Task<ResponseResultInfo<List<WordTemplateInfo>>> SearchWordTemplateList(string keyword) { ResponseResultInfo<List<WordTemplateInfo>> respResult = new ResponseResultInfo<List<WordTemplateInfo>>(); try { List<WordTemplateInfo> list = await _context.WordTemplateInfoes.Where(x => x.Type == TemplateType.Public && x.Name.Contains(keyword)).OrderByDescending(x => x.CreateTime).ToListAsync(); respResult.IsSuccess = true; respResult.Result = list; return respResult; } catch (Exception ex) { //LogHelper.ErrorWriteLine("Something wrong. The exception message::{0}", ex); respResult.IsSuccess = false; respResult.Message = string.Format("Something wrong. The exception message::{0}", ex.Message); return respResult; } }
命令行转到项目目录,运行如下命令
dotnet run
可使用前端调试利器Postman来测试:
API项目运行的具体地址须要记一下,后面作Add-In的时候要用到。具体代码请参考Github。
为了支持Add-in可以跨域访问咱们的接口,还须要安装如下的库:
而后在Startup.cs的ConfigureServices方法中添加如下代码:
#region 跨域 services.AddCors(options => options.AddPolicy("AllowCrossDomain", builder => builder.WithOrigins().AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin().AllowCredentials()) ); #endregion
在须要跨域的WordTemplateController上添加一行:
[EnableCors("AllowCrossDomain ")]
这样api就能够支持跨域访问了。
有了API,就能够开发Add-In部分了。开篇说到,Add-In其实是一个Web App,经过JavaScript操做Office文档对象,具体到这个项目来讲,就是使用异步的js去查询、上传、搜索存在服务器上的模板文件,并动态的对当前Word文档进行操做。
微软在Github上开源了这个JavaScript API:https://github.com/OfficeDev/Office-js-docs_zh-cn/tree/staging,相关文档:https://msdn.microsoft.com/zh-cn/library/office/fp142185.aspx
下面来开发Add-In部分。
在解决方案上点击右键,添加一个Word Web外接程序:
添加完成后,多了两个项目:
其中一个是清单文件,带Web后缀的就是Web App了。
清单文件是很是重要的一个文件,描述加载项的全部设置。这个文件是自动生成的,但须要咱们手动修改一些地方。好在文件中都有注释,因此修改还比较容易:
最重要的是修改SourceLocation这个节点,这个地址设置的是Web App托管的位置。在Web端开发并部署后,要将这个节点改成正确的位置才能发布。下面这个节点也要改掉。
能够先运行一下这个模板试试,直接F5:
点击此处就能够调出这个插件:
会自动打开Word并加载这个插件,文档中的文本就是插件插入的。那么是哪里的代码起做用的呢?
打开Home.js文件,找到以下代码:
function loadSampleData() { // Run a batch operation against the Word object model. Word.run(function (context) { // Create a proxy object for the document body. var body = context.document.body; // Queue a commmand to clear the contents of the body. body.clear(); // Queue a command to insert text into the end of the Word document body. body.insertText( "This is a sample text inserted in the document", Word.InsertLocation.end); // Synchronize the document state by executing the queued commands, and return a promise to indicate task completion. return context.sync(); }) .catch(errorHandler); }
这里的Word就是JavaScript API提供的对象,能够方便的对当前文档内容进行操做。这样思路就有了,能够经过JavaScript动态去调用Web API获取查询结果,将查询到的文档内容插入到当前文档中,就实现了最初的目的。同时还能够将当前文档的内容保存为模板上传到服务器上进行分享,一个完整功能的sample已经呼之欲出了。
咱们使用最新的Angular4来开发前端页面。固然若是使用JQuery的话也能够,但如今已经有点out了不是吗?使用Angular能够快速开发一个MVVM架构的单页面WebApp,很是适合这个需求。
这个demo的部分代码参考微软开源的一个项目:https://github.com/OfficeDev/Office-Add-in-UX-Design-Patterns-Code
Angular上手曲线仍是有点陡的,官方给出了Angular CLI工具,能够快速搭建一个Angular应用。首先安装TypeScript:
npm install -g typescript
而后安装Angular CLI:https://github.com/angular/angular-cli
npm install -g @angular/cli
运行如下命令建立一个Angular项目:
ng new WordTemplateHelperSource
而后使用cd WordTemplateHelperSource 命令转到项目目录,运行如下命令:
npm install
这个命令会安装ng项目所需的依赖,若是安装不成功,建议切换成淘宝npm镜像进行安装。
使用如下命令运行ng项目:
ng serve
能够在Chorme浏览器中浏览http://localhost:4200来查看效果:
注意若是在IE中浏览是不正常的,这个问题咱们到最后一节再给出解决办法。
为何不直接在WordTemplateHelperWeb建呢?由于Angular应用还要进行打包,会在项目目录下生成dist目录,这才是正式要运行的部分。因此等开发完成后,将生成的dist目录内的文件拷到WordTemplateHelperWeb就能够了。
在开发Angular的过程当中,推荐使用VS Code,对TypeScript和Angular的支持都很是好。
由于本篇文章不是Angular的开发教程,因此Angular的具体知识这里就不展开详述了,感兴趣的话能够自行下载Github代码运行便可。
为了操做Word文件,咱们须要将其封装成服务。使用如下命令添加一个service:
ng g service services\word-document\WordDocument
这样会在app目录中的相应路径中生成一个名为WordDocumentService的服务。与此相似,生成其余的几个service。其中主要的几个方法以下:
查询搜索的方法:
/** * search * * @param {string} keyword * @returns {Promise<ResponseResultInfo<Array<WordTemplateInfo>>>} * * @memberOf WordTemplateApiService */ searchWordTemplateList(keyword: string): Promise<ResponseResultInfo<Array<WordTemplateInfo>>> { let url = `${AppGlobal.getInstance().server}/SearchWordTemplateList?keyword=${keyword}`; let promise = this.httpService.get4Json<ResponseResultInfo<Array<WordTemplateInfo>>>(url); return promise; }
这样能够获得服务器上存储的文档模板,实际是以Ooxml格式保存的string。
对于这个sample来讲,使用Office JavaScript API并无太难的东西,主要用到了两个方法:getOoxml()和insertOoxml(),前者能够读取当前word文档的Ooxml格式,后者能够设置当前word文档的Ooxml格式。Ooxml就是Office2007以后版本使用的格式,如docx这种。
原API提供的都是callback函数,为了使用方便我将其封装成Promise:
/** * get the ooxml of the doc * * * @memberOf WordDocumentService */ getOoxml() { // Run a batch operation against the Word object model. return Word.run(function (context) { // Create a proxy object for the document body. var body = context.document.body; // Queue a commmand to get the HTML contents of the body. var bodyOOXML = body.getOoxml(); // Synchronize the document state by executing the queued commands, // and return a promise to indicate task completion. // return context.sync().then(function () { // console.log("Body HTML contents: " + bodyHTML.value); // return bodyHTML.value; // }); return context.sync().then(() => { return bodyOOXML.value }); }) .catch(function (error) { console.log("Error: " + JSON.stringify(error)); if (error instanceof OfficeExtension.Error) { console.log("Debug info: " + JSON.stringify(error.debugInfo)); } return ""; }); } /** * set the ooxml of the doc * * @param {string} ooxml * * @memberOf WordDocumentService */ setOoxml(ooxml: string) { // Run a batch operation against the Word object model. Word.run(function (context) { // Create a proxy object for the document body. var body = context.document.body; // Queue a commmand to insert OOXML in to the beginning of the body. body.insertOoxml(ooxml, Word.InsertLocation.replace); // Synchronize the document state by executing the queued commands, // and return a promise to indicate task completion. return context.sync().then(function () { console.log('OOXML added to the beginning of the document body.'); }); }) .catch(function (error) { console.log('Error: ' + JSON.stringify(error)); if (error instanceof OfficeExtension.Error) { console.log('Debug info: ' + JSON.stringify(error.debugInfo)); } }); }
当搜索到合适的模板后,能够单击按钮,调用setOoxml()方法,将其插入到当前word文档中:
applyTemplate(template: WordTemplateInfo) { this.wordDocument.setOoxml(template.TemplateContent); }
这样就完成了应用模板的功能。
若是要实现将当前文档的内容保存为模板上传到服务器上,就能够调用getOoxml()方法获得当前文档的Ooxml格式文本,上传到服务器保存便可。至于其余的加为收藏、添加为机构模板、设置为我的模板等都是设置模板属性更新了,具体代码再也不赘述。
还有一点须要注意的是,开发的时候,这里的服务器地址要写刚才咱们开发的ASP.NET Core的地址。
对于一个Office Add-in来讲,具备简洁美观、与Office统一的UI是必须的。微软推荐使用Fabric UI来实现统一的界面样式,详见:https://dev.office.com/fabric
这里提供了样式、图标、设计规范等不少资源,甚至还提供了React版的组件,若是使用React开发的话直接拿来用就能够了。这个demo是直接引用的style文件,配置在.angular-cli.json文件中:
应用后就变成这样子:
刚才只是在一个新项目里开发了一个静态Web App,还要将其打包,复制到WordTemplateHelperWeb项目中。使用ng build –prod来打包Angular应用。打包后的文件会输出到dist目录下:
注意还有一个须要注意的地方,若是仅这样打包的话,是不支持IE浏览器的,但Office Add-In实际上内置的浏览器就是IE内核,因此咱们须要作以下修改,找到src目录中的polyfills.ts文件,将下面部分的注释取消:
还要根据提示,运行npm install命令安装几个必须的依赖。这样才能在IE系列浏览器中正常运行。再次运行ng build –prod进行打包。--prod参数的意义是以生产模式进行build,这样生成的代码体积更小,运行速度更快。
将WordTemplateHelperWeb项目中的原文件除了Web.config外,所有删除。把dist目录中的文件复制过来。
虽然本机开发时能够直接调试运行,但为了模拟真实的使用状况,咱们把这个Web App也正式发布一下。若是咱们有Azure或其余主机的话就直接部署到服务器上,如今只用本机IIS来承载这个Web App:
这样该Add-In的地址就是:http://localhost/WordTemplateHelperWeb,
下面把api运行起来,进入WordTemplateHelperApi目录,运行dotnet run命令:
这样API项目的地址是:http://localhost:5000/api/
这两个地址不要混淆。刚才在打包WebApp的时候也要注意,在common\app-global.ts文件中的api地址也要改为和实际api地址同样的才能够:
/** * api url * * @type {string} * @memberOf AppGlobal */ public server: string = "http://localhost:5000/api/WordTemplate";
如今打开WordTemplateHelperManifest清单文件,修改以下位置:
这里填的是Add-In的地址,必定不要搞错了。
如今能够从新运行Add-In项目了,将启动项目设置为WordTemplateHelper,运行:
咱们能够粘贴一个模板,并上传到服务器上:
点击Upload按钮便可将当前文档做为模板上传到服务器上分享。
搜索到相应的模板后,点击apply按钮便可将模板内容插入到当前文档。
咱们能够搜索模板,添加本身的模板,并将模板内容应用到当前文档中。针对组织和我的还能够分别进行管理,个人设想是,这个小插件可以作成一个模板商店之类的平台,用户能够自由的交换彼此的文档模板,并能够收藏、添加到本人组织的模板库中等等。稍加扩展就能够作成一个正式产品了。
在页面加载时能够加一个载入提示,使用户体验更加友好。具体代码可参考index.html中的css样式。
这篇文章拖了好久,去年的比赛,今年才把过程整理出来,实在很想对陈老师说一声抱歉^_^。Office Add-In是一个比较新的开发领域,跟之前的开发方式有所不一样,但熟悉前端的同窗能够迅速进入这个领域,实际上就是写网页。这个实例从后端接口到前台实现,是一个比较完整的项目,但愿对Office开发有兴趣的同窗下载代码研究一下,开发出更加实用的Add-In。由于这个项目并无实际部署,因此没有上传到商店中。下载代码的用户请勿用于商业用途。特此说明。
Github地址:https://github.com/yanxiaodi/WordTemplateHelper