使用 Web API 作爲動態 TypeScript 編譯器運行環境

轉自:http://www.oschina.net/question/12_72796?from=20121014

 

使用 Web API 作爲動態 TypeScript 編譯器運行環境,因爲你不需要對 Typescript 進行預編譯。

已經有很多社區的文章在介紹 Typescript 這個新的語言,這是理所當然的 - 因爲它解決了很多JavaScript的問題,儘管處於起步階段,卻已經顯示了巨大的潛力。

Typescript 要求你必須預編譯來生成 JavaScript 代碼,現在也可以直接在瀏覽器上動態編譯,但你必須引用 Typescript.js 這個 JS 編譯器,這個編譯器有 250Kb 大小,這可能是一顆很難嚥下的藥丸。

好在,通過 ASP.NET 的瑞士軍刀 —— Web API ,我們可以實現對 Typescript 動態編譯爲 Javascript。

思路

爲了不用再每次修改 Typescript 後都要手工編譯,我們將透過 Web API 來爲我們完成這項工作,使用的是定製的 MediaTypeFormatter.

你所需要做的就是通過一個特定的預先配置好的 Web API 路由/控制器來引用這個 JS 編譯器腳本(Typescript.js),然後讓 Web API 管道通過 MediaTypeFormatter 來完成這項重任務。

路由和控制器

我們在 HTML 中引用編譯後的 js 文件如下:

1 <script type="text/javascript" src="/dynamic/scripts/demo.js"></script>

爲了實現這個目的,新建一個典型的 MVC4, Web API 項目。

需要一個定製的路由和一個簡單的控制器:

1 config.Routes.MapHttpRoute(
2     name: "DynamicScripts",
3     routeTemplate: "dynamic/{controller}/{name}.{ext}",
4     defaults: new { name = RouteParameter.Optional, ext = RouteParameter.Optional },
5     constraints: new { controller = "Scripts" }
6 );
1 public class ScriptsController : ApiController
2 {
3     public string Get(string name)
4     {
5         return name;
6     }
7 }

這個路由可以讓我們傳遞一個 name 參數和一個擴展(用於匹配所需的 filename.js),然後控制器簡單的將文件對應的請求重定向到 formatter 中。

插件

爲了讓上述思路可行,我們需要命令行的 Typescript 編譯器,可從 官方網站 上獲取,選擇中間那個 (Plugins),推薦 Visual Studio 2012,但 2010 也沒關係,我們只關心命令行工具而已。

一旦安裝成功,你會找到一個名爲 TCS.exe 的可執行文件,默認位於 C:\Program Files (x86)\Microsoft SDKs\TypeScript\0.8.0.0\. 將所有編譯器文件拷貝到解決方案目錄下的 TS 文件夾。

格式化器/編譯器

注意下面的代碼應根據你特定的需求進行調整(包括持久化機制、錯誤處理等等):

01 public class TypeScriptMediaTypeFormatter : MediaTypeFormatter
02 {
03         private static readonly ObjectCache Cache = MemoryCache.Default;
04   
05         public TypeScriptMediaTypeFormatter()
06         {
07             this.AddUriPathExtensionMapping("js", "text/html");
08         }
09   
10         public override void SetDefaultContentHeaders(Type type, System.Net.Http.Headers.HttpContentHeaders headers, System.Net.Http.Headers.MediaTypeHeaderValue mediaType)
11         {
12             headers.ContentType = new MediaTypeHeaderValue("application/javascript");
13         }
14   
15         public override bool CanReadType(Type type)
16         {
17             return false;
18         }
19   
20         public override bool CanWriteType(Type type)
21         {
22             return type == typeof(string);
23         }
24   
25         public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) {
26     //TODO
27     }
28 }

在我們開始寫數據到流前,所有代碼的執行都是很耗資源的,注意我們設置了一些默認值,我們添加了 UriPathExtensionMapping 這樣格式化器就可以處理所有 .js 請求。所有的輸出的內容 content-type 將是 application/javascript ,告訴瀏覽器這些都是 js 文件。也支持請求 text/html, 某些瀏覽器可能會這樣。

我們只支持序列化(單向的格式化,非反序列化),而且只接受字符串。

WriteToStreamAsync

MediaTypeFormatter 方法根據如下流程將數據寫到流中:

  1. 文件名(Typescript 文件)
  2. 檢查 TS 文件是否存在
  3. 檢查緩存,如果相應的文件已存在則使用MD5 checksum 來確保文件沒有改動
  4. 如果沒改動則直接從緩存中返回內容
  5. 如果改動了,或者緩存文件不存在則使用 tcs.exe 編譯並返回 JS
  6. 將第5步生成的 JS 和 MD5 checksum 內容保存到緩存中以便繼續使用
01 public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
02 {
03    var serverPath = HttpContext.Current.Server.MapPath("~/tsc");
04    var filepath = Path.Combine(serverPath, value.ToString() + ".ts");
05    var jsfilepath = Path.Combine(serverPath, value.ToString() + ".js");
06   
07    var tcs = new TaskCompletionSource<object>();
08   
09    if (File.Exists(filepath))
10    {
11       string cachedItem = CheckCache(filepath, value as string);
12   
13       if (cachedItem != null)
14       {
15          using (var writer = new StreamWriter(writeStream))
16          {
17             writer.Write(cachedItem);
18          }
19       }
20       else
21       {
22          var typescriptCompiler = new ProcessStartInfo
23          {
24             UseShellExecute = false,
25             RedirectStandardError = true,
26             FileName = Path.Combine(serverPath, "tsc.exe"),
27             Arguments = string.Format("\"{0}\"", filepath)
28          };
29   
30          var process = Process.Start(typescriptCompiler);
31          var result = process.StandardError.ReadToEnd();
32          process.WaitForExit();
33   
34          if (string.IsNullOrEmpty(result))
35          {
36             using (var filestream = new FileStream(jsfilepath, FileMode.Open, FileAccess.Read))
37             {
38                filestream.CopyTo(writeStream);
39                var fileinfo = new Dictionary<string, string>();
40                fileinfo.Add("md5", ComputeMD5(filepath));
41   
42                using (var reader = new StreamReader(filestream))
43                {
44                   filestream.Position = 0;
45                   var filecontent = reader.ReadToEnd();
46                   fileinfo.Add("content", filecontent);
47                }
48   
49                Cache.Set(value as string, fileinfo, DateTime.Now.AddDays(30));
50             }
51          }
52          else
53          {
54             throw new InvalidOperationException("Compiler error: " + result);
55          }
56       }
57    }
58               
59    tcs.SetResult(null);
60    return tcs.Task;
61 }
62   
63 private string CheckCache(string filepath, string filename)
64 {
65    var md5 = ComputeMD5(filepath);
66    var itemFromCache = Cache.Get(filename) as Dictionary<string, string>;
67   
68    if (itemFromCache != null)
69    {
70       if (itemFromCache["md5"] == md5)
71       {
72          return itemFromCache["content"];
73       }
74    }
75    return null;
76 }
77   
78 private string ComputeMD5(string filename)
79 {
80    var sb = new StringBuilder();
81    using (var file = new FileStream(filename, FileMode.Open, FileAccess.Read))
82    {
83       var md5 = new MD5CryptoServiceProvider();
84       var bytes = md5.ComputeHash(file);
85   
86       for (int i = 0; i < bytes.Length; i++)
87       {
88          sb.Append(bytes[i].ToString("x2"));
89       }
90    }
91   
92    return sb.ToString();
93 }

這樣做的好處是文件修改時我們只需要編譯一次就可以重複使用,這裏選用的是直接在內存中緩存,你也可以使用其他方式,例如返回磁盤中的 js 文件之類的。原則上,我們是通過 MD5 checksum 來確定文件是否修改。

注意這裏使用了 TCS 進程的 StandardError 屬性來判斷編譯器運行成功運行,如果編譯過程中發生任何錯誤,該屬性將會包含詳細的錯誤信息,否則就是空的。

測試

假設有如下的 TS 文件,名爲 demo.ts:

1 class Person {
2    constructor(public name) { }
3    sing(text) {
4        return this.name + " sings " + text;
5    }
6 }

我在 HTML 頁面中引用如下,使用相同的名稱,只是將 ts 擴展名改爲 js,這樣該請求就會觸發控制器調用編譯方法:

1 <script type="text/javascript" src="/dynamic/scripts/demo.js"></script>

現在 Web API 將即時編譯 demo.ts 並生成所需的 js 輸出到瀏覽器,而我們並沒有手工去編譯它:

而且 JS 內容是緩存的,以後再次刷新頁面無需重新編譯 ts 文件。

我的調用結果:

如果修改了 Typescript 代碼:If I change the Typescript code to something else – i.e. let’s modify the sing method:

1 class Person {
2 constructor ( public name ) { }
3 sing ( text ) {
4 return this . name + " sings " + text + " and it's embarassing." ;
5 }
6 }

我不需要重新編譯,只需要刷新頁面,因爲 MD5 checksum 不匹配,因此 TS 會自動重新編譯並生成新的 JS :

總結

我們前面提到的,Typescript 可使用純 JavaScript 來編譯生成 JavaScript,但因爲 JS 編譯器本身有 250Kb 大小,因此採用了這種方法來避免編譯器的加載變得讓人無法接受。

當然,如果你使用的是其他的 Web 開發技術,也可以參考這個思路來實現。

英文原文OSCHINA原創翻譯

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章