一个选项:Angular现在可以在模板中进行自动补全和错误检查
非常感谢,我是 @Quramy。
好的,今天我们再来谈谈Angular + 编辑器相关的事情。听说如果称呼为Angular 2会被人责备,所以我们称之为Angular。
让你先看到成果是最快的方法。看看这个吧。
我在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进行修改的癖好。
安装步骤
以下是应用程序的实现方式,但为了不太感兴趣或时间有限的人,我只贴上了适用步骤。
-
- 打开终端并移动到(使用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中。
-
- 将TypeScript的createLanguageService函数包装进来
-
- 用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人左右吧。