使用 Web API 作为动态 TypeScript 编译器运行环境

jopen 11年前

使用 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 文件如下:

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

为了实现这个目的,新建一个典型的 MVC4, Web API 项目。

需要一个定制的路由和一个简单的控制器:

config.Routes.MapHttpRoute(      name: "DynamicScripts",      routeTemplate: "dynamic/{controller}/{name}.{ext}",      defaults: new { name = RouteParameter.Optional, ext = RouteParameter.Optional },      constraints: new { controller = "Scripts" }  );

public class ScriptsController : ApiController  {      public string Get(string name)      {          return name;      }  }

这个路由可以让我们传递一个 name 参数和一个扩展(用于匹配所需的 filename.js),然后控制器简单的将文件对应的请求重定向到 formatter 中。

插件

为了让上述思路可行,我们需要命令行的 Typescript 编译器,可从 官方网站 上获取,选择中间那个 (Plugins),推荐 Visual Studio 2012,但 2010 也没关系,我们只关心命令行工具而已。

一旦安装成功,你会找到一个名为 TCS.exe 的可执行文件,默认位于 C:\Program Files (x86)\Microsoft SDKs\TypeScript\0.8.0.0\. 将所有编译器文件拷贝到解决方案目录下的 TS 文件夹。

格式化器/编译器

注意下面的代码应根据你特定的需求进行调整(包括持久化机制、错误处理等等):

public class TypeScriptMediaTypeFormatter : MediaTypeFormatter  {          private static readonly ObjectCache Cache = MemoryCache.Default;            public TypeScriptMediaTypeFormatter()          {              this.AddUriPathExtensionMapping("js", "text/html");          }            public override void SetDefaultContentHeaders(Type type, System.Net.Http.Headers.HttpContentHeaders headers, System.Net.Http.Headers.MediaTypeHeaderValue mediaType)          {              headers.ContentType = new MediaTypeHeaderValue("application/javascript");          }            public override bool CanReadType(Type type)          {              return false;          }            public override bool CanWriteType(Type type)          {              return type == typeof(string);          }            public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) {      //TODO      }  }

在我们开始写数据到流前,所有代码的执行都是很耗资源的,注意我们设置了一些默认值,我们添加了 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 内容保存到缓存中以便继续使用
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)  {     var serverPath = HttpContext.Current.Server.MapPath("~/tsc");     var filepath = Path.Combine(serverPath, value.ToString() + ".ts");     var jsfilepath = Path.Combine(serverPath, value.ToString() + ".js");       var tcs = new TaskCompletionSource<object>();       if (File.Exists(filepath))     {        string cachedItem = CheckCache(filepath, value as string);          if (cachedItem != null)        {           using (var writer = new StreamWriter(writeStream))           {              writer.Write(cachedItem);           }        }        else        {           var typescriptCompiler = new ProcessStartInfo           {              UseShellExecute = false,              RedirectStandardError = true,              FileName = Path.Combine(serverPath, "tsc.exe"),              Arguments = string.Format("\"{0}\"", filepath)           };             var process = Process.Start(typescriptCompiler);           var result = process.StandardError.ReadToEnd();           process.WaitForExit();             if (string.IsNullOrEmpty(result))           {              using (var filestream = new FileStream(jsfilepath, FileMode.Open, FileAccess.Read))              {                 filestream.CopyTo(writeStream);                 var fileinfo = new Dictionary<string, string>();                 fileinfo.Add("md5", ComputeMD5(filepath));                   using (var reader = new StreamReader(filestream))                 {                    filestream.Position = 0;                    var filecontent = reader.ReadToEnd();                    fileinfo.Add("content", filecontent);                 }                   Cache.Set(value as string, fileinfo, DateTime.Now.AddDays(30));              }           }           else           {              throw new InvalidOperationException("Compiler error: " + result);           }        }     }                   tcs.SetResult(null);     return tcs.Task;  }    private string CheckCache(string filepath, string filename)  {     var md5 = ComputeMD5(filepath);     var itemFromCache = Cache.Get(filename) as Dictionary<string, string>;       if (itemFromCache != null)     {        if (itemFromCache["md5"] == md5)        {           return itemFromCache["content"];        }     }     return null;  }    private string ComputeMD5(string filename)  {     var sb = new StringBuilder();     using (var file = new FileStream(filename, FileMode.Open, FileAccess.Read))     {        var md5 = new MD5CryptoServiceProvider();        var bytes = md5.ComputeHash(file);          for (int i = 0; i < bytes.Length; i++)        {           sb.Append(bytes[i].ToString("x2"));        }     }       return sb.ToString();  }

这样做的好处是文件修改时我们只需要编译一次就可以重复使用,这里选用的是直接在内存中缓存,你也可以使用其他方式,例如返回磁盘中的 js 文件之类的。原则上,我们是通过 MD5 checksum 来确定文件是否修改。

注意这里使用了 TCS 进程的 StandardError 属性来判断编译器运行成功运行,如果编译过程中发生任何错误,该属性将会包含详细的错误信息,否则就是空的。

测试

假设有如下的 TS 文件,名为 demo.ts:

class Person {     constructor(public name) { }     sing(text) {         return this.name + " sings " + text;     }  }

我在 HTML 页面中引用如下,使用相同的名称,只是将 ts 扩展名改为 js,这样该请求就会触发控制器调用编译方法:

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

现在 Web API 将即时编译 demo.ts 并生成所需的 js 输出到浏览器,而我们并没有手工去编译它:

使用 Web API 作为动态 TypeScript 编译器运行环境

而且 JS 内容是缓存的,以后再次刷新页面无需重新编译 ts 文件。

我的调用结果:

使用 Web API 作为动态 TypeScript 编译器运行环境

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

class Person {  constructor ( public name ) { }  sing ( text ) {  return this . name + " sings " + text + " and it's embarassing." ;  }  }

我不需要重新编译,只需要刷新页面,因为 MD5 checksum 不匹配,因此 TS 会自动重新编译并生成新的 JS :

使用 Web API 作为动态 TypeScript 编译器运行环境

总结

我们前面提到的,Typescript 可使用纯 JavaScript 来编译生成 JavaScript,但因为 JS 编译器本身有 250Kb 大小,因此采用了这种方法来避免编译器的加载变得让人无法接受。

当然,如果你使用的是其他的 Web 开发技术,也可以参考这个思路来实现。

英文原文,OSCHINA原创翻译