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