2016年10月我參加了在北京舉行的DevDays Asia 2016 - Office 365應用開發”48小時黑客馬拉松“,我開發的一個Word Add-In Demo——WordTemplateHelper獲得了二等獎。在會場有幸結識了陳希章老師,在與陳老師的交流中受益良多,得知陳老師在準備一 ...
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 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,對應資料庫中的表,實體對應表中的行。
5.使用依賴註入註冊上下文
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,用於測試。當需要正式部署時,這裡需要更改為正式資料庫伺服器的地址及用戶名密碼。
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為例:
[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。
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
下麵來開發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:
/** * 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的地址。
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