一个选项:Angular现在可以在模板中进行自动补全和错误检查

非常感谢,我是 @Quramy。

好的,今天我们再来谈谈Angular + 编辑器相关的事情。听说如果称呼为Angular 2会被人责备,所以我们称之为Angular。

让你先看到成果是最快的方法。看看这个吧。

screencast

我在Angular组件的模板中添加了属性名称补全和错误检查的功能,如您所见。

如果你是Visual Studio Code的用户,请忽略上方的Vim截图,并且不需要再继续阅读。你可以在https://github.com/angular/vscode-ng-language-service 找到适用于VSC的插件,只需安装这个就可以了。

今天的目标读者是那些使用Emacs、Vim、Sublime Text等编辑器来编写Angular代码的人们。为什么有这样的限制呢?因为这些编辑器通常使用了名为tsserver的TypeScript语言支持服务,而本次讨论正涉及这个服务。回想起来,就在2015年的这个时候,我还写过一篇关于修改TypeScript以便能够导入jspm模块的文章。看来我在年末的时候总是想要对TypeScript进行修改的癖好。

安装步骤

以下是应用程序的实现方式,但为了不太感兴趣或时间有限的人,我只贴上了适用步骤。

    1. 打开终端并移动到(使用Angular CLI等创建的)项目的根目录。

使用npm install typescript @angular/language-service reflect-metadata命令来安装所需的NPM包。

执行以下命令:

curl 'https://raw.githubusercontent.com/Quramy/ng-tsserver/master/ng-tsserver' | bash

只需一个选项,以下是一种可能的翻译:
这样一来,已在第2步中安装的tsserver就会被打上补丁,并且添加了对Angular的编辑器支持。
大多数TypeScript编辑器插件都允许设置tsserver的位置,因此请在各自的编辑器设置中配置为使用本地安装的tsserver。

实现的方式是怎样的?

這裡是關於如何實現本次功能的自誇故事。

首先,对于Angular的语言支持功能,我们使用了一个名为@angular/language-service的NPM包。从其名称中可以看出,它是由Angular核心团队制作的,从2.3版本开始添加。顺便提一下,它的作者是chuckjaz。他主要负责编译器方面的工作。

顺便说一下,由于只有对Angular感兴趣且从事编辑器插件制作的人才直接使用这个模块,所以它没有任何文档甚至连一个README都没有。

因为我完全不知道如何使用它,所以我随便翻阅了一下.d.ts文件,然后找到了以下内容。

import * as ts from 'typescript';
/** A plugin to TypeScript's langauge service that provide language services for
 * templates in string literals.
 *
 * @experimental
 */
export declare class LanguageServicePlugin {
    private serviceHost;
    private service;
    private host;
    static 'extension-kind': string;
    constructor(config: {
        host: ts.LanguageServiceHost;
        service: ts.LanguageService;
        registry?: ts.DocumentRegistry;
        args?: any;
    });
    /**
     * Augment the diagnostics reported by TypeScript with errors from the templates in string
     * literals.
     */
    getSemanticDiagnosticsFilter(fileName: string, previous: ts.Diagnostic[]): ts.Diagnostic[];
    /**
     * Get completions for angular templates if one is at the given position.
     */
    getCompletionsAtPosition(fileName: string, position: number): ts.CompletionInfo;
}

看起来非常符合TypeScript的LanguageService的定义。事实上,在TypeScript本身的.d.ts文件中,在解析后可以找到以下描述。

namespace ts {
    // 一部抜粋
    function createLanguageService(host: LanguageServiceHost, documentRegistry?: DocumentRegistry): LanguageService;

    interface LanguageService {
        getSyntacticDiagnostics(fileName: string): Diagnostic[];
        getCompletionsAtPosition(fileName: string, position: number): CompletionInfo;
        // 他にも色々メソッドが生えている
    }
}

因此,如果按照下一个计划进行,似乎可以将Angular LanguageService整合到tsserver中。

    1. 将TypeScript的createLanguageService函数包装进来

 

    1. 用Angular的LanguageService的方法对原有的createLanguageService创建的LanguageService进行包装

 

    将第2步中包装的createLanguageService传给tsserver来使用

现在,第三个问题是个棘手的部分。我也考虑过fork TypeScript本身并进行自定义构建的方式,但这种方法对于其他人来使用来说太过困难。
另外,一旦调用tsserver,它就会立即启动,并没有提供从外部传入选项的机制。
虽然TypeScript还附带了一个名为tsserverlibrary.js的工具,但它需要自己实现关键的服务器部分,所以这次我们放弃选择它。

为了实现将TypeScript核心的.js文件都放置在名为ts的命名空间(内部模块)中,我采用了在ts.createLanguageService中预先添加Property Descriptor的方法。

const NgLanguageServicePlugin = require("@angular/language-service")().default;

if (typeof ts === "undefined") ts =  {};
var delegate;

Object.defineProperty(ts, "createLanguageService", {
  get: function() {
    return function(host, documentRegistry) {
      const ls = delegate(host, documentRegistry);
      const nglsp = new NgLanguageServicePlugin({
        host: host,
        service: ls,
        registry: documentRegistry
      });

      // オリジナルのメソッドを退避
      const completionFn = ls.getCompletionsAtPosition;
      const samnticCheckFn = ls.getSemanticDiagnostics;

      // LanguageServiceのメソッドをラップ
      ls.getCompletionsAtPosition = (filename, position) => {
        const ngResult = nglsp.getCompletionsAtPosition(filename, position);
        if (ngResult) return ngResult;
        return completionFn(filename, position);
      };
      ls.getSemanticDiagnostics = (fileName) => {
        return nglsp.getSemanticDiagnosticsFilter(fileName, samnticCheckFn(fileName));
      };
      return ls;
    }
  },
  set: function(v) {
    delegate = v;
  },
  configurable: true,
  enumerable: true
});
var ts; // tsserver.jsに定義されてる

这样做的话,在tsserver的实现中,当调用ts.createLanguageService的位置时,上述getter将会起作用,并返回已封装的LanguageService。执行安装步骤的shell脚本只是为了将已安装的tsserver.js追加到上述的.js中,并进行替换。

(function(ts) {
  // tsserverの本体実装
  // ↓みたいのがどっかにいる
  // ts.createLanguageService = function() {...};
})(ts || {}); // この時点で ts.LanguageServicePluginにはProperty Descriptorが適用されている

結尾

这次我描述了如何将Angular的LanguageService集成到TypeScript的LanguageService中,并使其可以从编辑器中使用。

Angular的LanguageService是一种能够在离线环境下执行@angular/compiler功能的工具,与AoT编译相似。在之前的Angular AoT指南中,我这样描述过。

使用ngc命令可以将HTML模板转换为.ngfactory.ts文件。在创建bundle时,需要将.ngfactory.ts文件经过JavaScript转译,并使用模块绑定器创建bundle文件。换句话说,在构建流程中,HTML模板(对应的是TypeScript)将进行类型检查。

如果使用编辑器和语言服务,就可以在比构建速度更快的时间内检查模板,所以没有理由不使用它!

如果有人知道更好的集成方法,请在评论或PR中告诉我,因为我意识到在正直地进入node_modules下的.js是相当危险的。

(2017年4月16日)追加記述:
由于在TypeScript 2.3.x中引入了语言服务插件(Language Service Plugin),现在可以进行LanguageService的替换,因此不再需要采用上述介绍的方法。
详细信息请参阅此条目。

注脚

看起来有点过时,但从 http://angularjs.blogspot.jp/2015/09/angular-2-survey-results.html 可以看出,Vim和Sublime用户占有相当大的比例。

Tsuquyomi(Vim)的情况下,默认使用本地安装的tsserver,无需配置。tide(Emacs)的情况下,请参考 https://github.com/ananthakumaran/tide#faq。

也许世界上对此感兴趣的人可能只有10人左右吧。

广告
将在 10 秒后关闭
bannerAds