之前曾經有個案例是要將使用者輸入的文案轉存成 EMAIL 的格式,下載後可以讓他們自行編輯再轉發出去,這邊就記錄一下重點
概略說明 因為先前採用的輸入介面是 CKEditor , 然後使用者如果在裡面編輯的話,實際上是存成 HTML,但如果它們又想要轉寄這些信件,CKEditor 的所見即所得就會造成很大的困擾。因為並不是每一家的 EMAIL 服務都能夠支援這些 HTML 語法與 CSS,大都是只有支援一部分,有許多新的語法並不支援。但是使用者又希望它們能夠在網站上直接將這些東西轉寄出去,當然如果直接這樣幹的話會有很多繞不過去的問題。
最終的解決方案就是將這些東西轉存成 eml 格式,然後讓使用者下載下來,讓它們自己編輯後再轉寄出去 附件的部分直接在編輯時附加在信件內;圖片則是轉 base64 後就可以直接顯示
前端就是點擊後呼叫後端並下載存成 EML 檔案
後端就是負責提供某個文章的 EMAIL 格式資料給前端
下載檔案 前端的部分比較單純,呼叫後端後將內容存檔即可
1 2 3 4 5 6 7 8 9 10 11 12 13 async mailDownload (id ) { let response = await ArticleModule .downloadEmail (id) const $a = document .createElement ("a" ) const url = URL .createObjectURL (response.data ) const fileName = response.headers ["content-disposition" ].split ("=" )[1 ] const currentFileName = fileName.replace (/"(.*)"/ , "$1" ).replace ("UTF-8''" , "" ) $a.download = decodeURIComponent (currentFileName) $a.href = url $a.click () setTimeout (() => URL .revokeObjectURL (url), 5000 ) }
轉存成 eml 格式 透過.EML - Email Generation in JS 範例,可以知道核心的解決方案就是像範例中一樣,把一些郵件的資訊放在最前面,指定好收件人、信件主旨,接著就是準備 HTML 的信件內容
又因為這邊要做兩三件事情,都是針對信件內容作加工處理,我覺得還蠻適合用裝飾模式,就順便練習一下
針對信件內容的圖片網址,轉 base64 編碼
針對信件內容,最前面加上一些信件的資訊,例如收件人、寄件人、信件主旨等等
針對信件內容的部分,將 CKEditor 的 CSS 附加上去
裝飾模式 大概就跟穿衣服一樣,所有穿衣服的動作都是圍繞著人這個主體,概念就是這樣而已,穿衣服的行為就是在人的身上穿衣服、穿褲子、穿內衣、戴手錶、穿襪子、穿鞋子這些事情而已,所以就是用下面的介面來表示這個行為
1 2 3 4 public interface IPeopleDecorator { string Decorate (string people ) ; }
用這個例子來說的話,裝飾前是:人
,裝飾後就會是:穿著運動上衣的人
,其他的裝飾也是一樣的概念
出門運動跑步,大概就會知道你要在你的身上弄好:衣服、褲子、鞋子,最後可能就會穿著運動上衣、運動褲、運動鞋的人
出門上班上課,大概就會知道你在身上要弄好的是:衣服、褲子、鞋子、背包,最後可能就會穿著襯衫、西裝褲、皮鞋、還拿個公事包的人
這只是個例子,不要太認真要帶什麼東西
所以用上面的概念,你就會知道有一個 人的裝飾工廠,可能有兩個方法,一個是準備出門運動的行頭 method、另外一個是準備出門上班上課的行頭 method
like this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public static class PeopleDecoratorFactory { public static IPeopleDecorator Sport () { return new PeopleDecorator(運動上衣,運動褲,運動鞋); } public static IPeopleDecorator Work () { return new PeopleDecorator(襯衫,西裝褲,皮鞋,公事包); } } ``` `PeopleDecorator`就是實際上穿衣服的行為,用程式碼的概念就是下面這樣 ```cs internal class PeopleDecorator : IPeopleDecorator { private readonly List<IPeopleDecorator> _decorators; public ContentDecorator (params IPeopleDecorator[] decorators ) { _decorators = decorators.ToList(); } public string Decorate (string content ) { string decoratedContent = content; foreach (var decorator in _decorators) { decoratedContent = decorator.Decorate(decoratedContent); } return decoratedContent; } }
最終要使用的時候就是向下面這樣呼叫就可以了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public string GoToPark (){ var people = "art" ; var decorator = PeopleDecoratorFactory.Sport(); var result = decorator.Decorate(people); return result; } public string GoToWork (){ var people = "art" ; var decorator = PeopleDecoratorFactory.Work(); var result = decorator.Decorate(people); return result; }
整體的概念及程式碼就是這樣了,下面用實際的範例來感覺一下
實際範例 輸入驗證的部分以及例外處理的部分因為不是本文重點,直接略過,若實際要用的話記得補上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public FileResult DownloadEmail (int id ){ var article = ArticleModule.Get(id); var decorator = ContentDecoratorFactory.CreateDecorator(article); var emailContent = decorator.Decorate(article.Content); var fileBytes = Encoding.UTF8.GetBytes(emailContent); var fileName = $"[{article.Type.GetDescription()} ] {article.Subject} .eml" ; return File(fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet, fileName); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static class ContentDecoratorFactory { private const string ImageUrlPattern = @"\/Common\/ArticleImage\/[a-zA-Z0-9]{32}" ; public static IContentDecorator CreateDecorator (ArticleEntity article ) { var pattern = new Regex(ImageUrlPattern); var imageUrlDecorator = new ArticleImageDecorator(pattern); var styleDecorator = new CkeditorStyleDecorator(); var htmlDecorator = new HtmlTagDecorator(article.Subject); return new ContentDecorator(imageUrlDecorator, styleDecorator, htmlDecorator); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 internal class articleImageDecorator : IContentDecorator { private readonly Regex _pattern; private IarticleFileInfoDao _articleFileInfoDao; private IarticleFileInfoDao articleFileInfoDao => _articleFileInfoDao ?? (_articleFileInfoDao = DataFactory.GetarticleImageInfoDAO()); public articleImageDecorator (Regex pattern ) { _pattern = pattern; } public string Decorate (string content ) { return _pattern.Replace(content, match => { var guid = match.Value.Split('/' ).Last(); articleFileInfo fileInfo = articleFileInfoDao.GetFileInfoByGuid(guid); if (fileInfo == null ) throw new MyException(ExceptionCode.NotFound, $"cannot found fileInfo by Guid:{match.Value} " ); var baseFolder = "D:\\article\\" var filePath = string .Concat($@"{baseFolder} {fileInfo.articleId} \" , $"{guid} .jpg" ); byte [] imageBytes = File.ReadAllBytes(filePath); string base64String = Convert.ToBase64String(imageBytes); return $"data:image/jpeg;base64,{base64String} " ; }); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 internal class CkeditorStyleDecorator : IContentDecorator { public string Decorate (string content ) { var sb = new StringBuilder(); sb.AppendLine("<style>" ); sb.AppendLine("</style>" ); sb.AppendLine(content); return sb.ToString(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 internal class HtmlTagDecorator : IContentDecorator { private readonly string _subject; public HtmlTagDecorator (string subject ) { _subject = subject; } public string Decorate (string content ) { var sb = new StringBuilder(); sb.AppendLine("To:" ); sb.AppendLine("Subject: " + _subject); sb.AppendLine("X-Unsent: 1" ); sb.AppendLine("Content-Type: text/html; charset=utf-8" ); sb.AppendLine("" ); sb.AppendLine(@"<html><head></head><body>" ); sb.AppendLine(content); sb.AppendLine("</body></html>" ); return sb.ToString(); } }
結論 整體上就是這樣子,看程式碼可能會有點難理解,不過如果概念懂了之後,用什麼辦法都沒關係,程式碼只是輔助而已,重點是能不能做到你想要的事情,這才是重點。關於裝飾模式有很多介紹的文章跟書籍都有談到,我也看了好多個,以前還寫了個 js 版本的JavaScript Decorator Pattern ,最後還是因為這個工作上實際應用上了,才比較有感覺,所以這邊就記錄一下,希望也對別人有幫助。