Office365开发系列——开发一个全功能的Word Add-In
2016年10月我參加了在北京舉行的DevDays Asia 2016 - Office 365應用開發”48小時黑客馬拉松“,我開發的一個Word Add-In Demo——WordTemplateHelper獲得了二等獎。在會場有幸結識了陳希章老師,在與陳老師的交流中受益良多,得知陳老師在準備一個Office解決方案系列后,我想把這個Demo的開發過程簡要介紹給大家,以支持陳老師的無私奉獻,也希望更多的開發者參與到Office365的開發中來。
Office相關開發主要可以參考這個地址:https://dev.office.com/getting-started
本篇文章主要介紹其中的Office加載項開發,即Office Add-ins:https://msdn.microsoft.com/ZH-CN/library/office/jj220082.aspx?
一、什么是Office Add-Ins
什么是Office Add-ins呢?在陳老師的上一篇文章中,對整個Office發展歷史都進行了梳理,我個人的理解就是,開發者可以在Office提供的平臺上,對Office做出一定的擴展以實現各種功能,比如之前錄制的宏,寫的VBS的腳本,某種意義上都可以看做是Office的Add-ins。當然這只是個人理解,不一定準確。目前的Office Add-Ins只支持Office2013以后的版本,開發方式也和以前的VBS有了很大的區別。
現在的Office Add-ins結構是這樣的:
一個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進行操作,能實現什么就取決于開發者的想象力了。
一個Word Add-In的實例:
?
二、Word Template Helper需求分析
我在得知有這個活動時,并沒有想好要做什么,一直到坐上赴京的高鐵,才慢慢有了一個想法,這個想法也是來自平時的工作需要。在工作中經常要撰寫大量的文檔,如各種軟件需求規格說明書、公函、文書、操作手冊等,這些文檔都有規定的格式,一般情況下我是將一些已經寫好的Word文檔保存在一個文件夾里當做模板,下次寫這種文檔的時候復制一份,刪刪減減的再改。為何不自己寫個程序,將這些具有固定模式的文檔作為Word模板呢?雖然Word也有自己的模板,但實際上是非常有限的,并不能完全滿足我們的需要。如果這個功能做成一個模板商店,大家可以自由上傳、分享各自的模板,也許會方便許多。
Word自帶的模板是這樣的:
這些通用模板對專業性比較強的工作來說是遠遠不夠的。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
?
四、Web API開發
VS2017已經正式發布了,我使用最新的.NET Core來實現Web API層。
1.新建項目
新建一個空白解決方案,命名為WordTemplateHelpe,然后在其中添加一個ASP.NET Core項目:
選擇Web API:
2.安裝EF Core
在nuget管理器中搜索安裝一下幾個Nuget包:
Microsoft.EntityFrameworkCore.SqlServer:EF Core SQL Server
Microsoft.EntityFrameworkCore.Tools:EF命令行工具
Microsoft.EntityFrameworkCore.Tools.DotNet:EF Core命令行工具
3.建立Models
目前最新的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上的示例。
4.創建數據庫上下文
有了Model后,需要指定哪些實體包含在數據模型中。添加一個Data文件夾,在其中創建一個名為WordTemplateContext.cs的文件:
? ?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,對應數據庫中的表,實體對應表中的行。
?
5.使用依賴注入注冊上下文
ASP.NET Core默認實現了依賴注入。要把剛才建立的WordTemplateContext注冊成服務,需要在Startup.cs中添加以下代碼:
注意要添加using Microsoft.EntityFrameworkCore;不然會找不到UseSqlServer方法。
數據庫連接字符串在appsettings.json中配置:
{ ?"ConnectionStrings": { ? ?"DefaultConnection": "Server=.;User ID=sa;Password=12QWasZX;Initial Catalog=WordTemplate;"}, ?"Logging": { ? ?"IncludeScopes": false, ? ?"LogLevel": { ? ? ?"Default": "Warning"}} }這里使用了LocalDb,用于測試。當需要正式部署時,這里需要更改為正式數據庫服務器的地址及用戶名密碼。
6.初始化數據庫
下面使用命令行初始化數據庫。在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);}?
7.創建API接口
現在寫個Controller看看。在Controller文件夾中添加一個控制器:
這里可以使用依賴注入,將數據庫上下文注入進來:
[Produces("application/json")][Route("api/WordTemplate/[action]")] ?? ?public class WordTemplateController : Controller{ ? ? ?
? ? ?private readonly WordTemplateContext _context; ? ? ? ? ? ?public WordTemplateController(WordTemplateContext context){_context = context;}
我們以一個搜索模板的api為例:
? ? ? ??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。
8.允許跨域訪問
為了支持Add-in能夠跨域訪問我們的接口,還需要安裝以下的庫:
然后在Startup.cs的ConfigureServices方法中添加以下代碼:
#region 跨域services.AddCors(options =>options.AddPolicy("AllowCrossDomain",builder => builder.WithOrigins().AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin().AllowCredentials())); ? ? ? ? ? ?#endregion在需要跨域的WordTemplateController上添加一行:
[EnableCors("AllowCrossDomain ")]?
這樣api就可以支持跨域訪問了。
?
五、Word Add-In開發
有了API,就可以開發Add-In部分了。開篇說到,Add-In實際上是一個Web App,通過JavaScript操作Office文檔對象,具體到這個項目來說,就是使用異步的js去查詢、上傳、搜索存在服務器上的模板文件,并動態的對當前Word文檔進行操作。
微軟在Github上開源了這個JavaScript API:https://github.com/Microsoft/Office-js-docs_zh-cn,相關文檔:https://msdn.microsoft.com/zh-cn/library/office/fp142185.aspx
開發步驟可參考:https://github.com/Microsoft/Office-js-docs_zh-cn/blob/master/docs/get-started/create-and-debug-office-add-ins-in-visual-studio.md
下面來開發Add-In部分。
1.新建Add-In項目
在解決方案上點擊右鍵,添加一個Word Web外接程序:
添加完成后,多了兩個項目:
其中一個是清單文件,帶Web后綴的就是Web App了。
2.設置manifest.xml
清單文件是非常重要的一個文件,描述加載項的所有設置。這個文件是自動生成的,但需要我們手動修改一些地方。好在文件中都有注釋,所以修改還比較容易:
最重要的是修改SourceLocation這個節點,這個地址設置的是Web App托管的位置。在Web端開發并部署后,要將這個節點改為正確的位置才能發布。下面這個節點也要改掉。
3.Web App分析
可以先運行一下這個模板試試,直接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已經呼之欲出了。
4.使用Angular
我們使用最新的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代碼運行即可。
5.添加操作Word文件的service
為了操作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:
?
當搜索到合適的模板后,可以單擊按鈕,調用setOoxml()方法,將其插入到當前word文檔中:
applyTemplate(template: WordTemplateInfo) { ? ?this.wordDocument.setOoxml(template.TemplateContent);}?
這樣就完成了應用模板的功能。
?
如果要實現將當前文檔的內容保存為模板上傳到服務器上,就可以調用getOoxml()方法得到當前文檔的Ooxml格式文本,上傳到服務器保存即可。至于其他的加為收藏、添加為機構模板、設置為個人模板等都是設置模板屬性更新了,具體代碼不再贅述。
還有一點需要注意的是,開發的時候,這里的服務器地址要寫剛才我們開發的ASP.NET Core的地址。
6.使用Fabric UI
對于一個Office Add-in來說,具有簡潔美觀、與Office統一的UI是必須的。微軟推薦使用Fabric UI來實現統一的界面樣式,詳見:https://dev.office.com/fabric
這里提供了樣式、圖標、設計規范等很多資源,甚至還提供了React版的組件,如果使用React開發的話直接拿來用就可以了。這個demo是直接引用的style文件,配置在.angular-cli.json文件中:
應用后就變成這樣子:
7.打包Add-in
剛才只是在一個新項目里開發了一個靜態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的地址,一定不要搞錯了。
6.運行測試
現在可以重新運行Add-In項目了,將啟動項目設置為WordTemplateHelper,運行:
我們可以粘貼一個模板,并上傳到服務器上:
點擊Upload按鈕即可將當前文檔作為模板上傳到服務器上分享。
搜索到相應的模板后,點擊apply按鈕即可將模板內容插入到當前文檔。
我們可以搜索模板,添加自己的模板,并將模板內容應用到當前文檔中。針對組織和個人還可以分別進行管理,我的設想是,這個小插件能夠做成一個模板商店之類的平臺,用戶可以自由的交換彼此的文檔模板,并可以收藏、添加到本人組織的模板庫中等等。稍加擴展就可以做成一個正式產品了。
7.載入加載動畫
在頁面加載時可以加一個載入提示,使用戶體驗更加友好。具體代碼可參考index.html中的css樣式。
六、小結
這篇文章拖了很久,去年的比賽,今年才把過程整理出來,實在很想對陳老師說一聲抱歉^_^。Office Add-In是一個比較新的開發領域,跟以前的開發方式有所不同,但熟悉前端的同學可以迅速進入這個領域,實際上就是寫網頁。這個實例從后端接口到前臺實現,是一個比較完整的項目,希望對Office開發有興趣的同學下載代碼研究一下,開發出更加實用的Add-In。因為這個項目并沒有實際部署,所以沒有上傳到商店中。下載代碼的用戶請勿用于商業用途。特此說明。?
Github地址:https://github.com/yanxiaodi/WordTemplateHelper
原文地址:http://www.cnblogs.com/yanxiaodi/p/7192280.html
.NET社區新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關注
總結
以上是生活随笔為你收集整理的Office365开发系列——开发一个全功能的Word Add-In的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 同理心是通往成功架构的桥梁
- 下一篇: .NET Exceptionless 日