Quantcast
Channel: Vimタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 5608

Angular で template内補完やエラーチェックできるようになったよ

$
0
0

どうも @Quramyです。

さて、今日も今日とてAngular + エディタ関連ネタです。そろそろAngular 2というと怒られるらしいのでAngularと書きます。

先に成果を見てもらうのが手っ取り早いですね。コイツを見れ。

screencast

見ての通り、Angular Componentのテンプレート中でプロパティ名補完とエラーチェックを行えるようにしてみました。

上図はVimのキャプチャですが、もし、これを読んでる貴方がVisual Studio Codeユーザーであるならば、これ以降は全く読まなくてよいです。https://github.com/angular/vscode-ng-language-serviceにVSC用のpluginが転がっているので、こいつを入れればいいさ。

今日の想定読者は、Emacs, Vim, Sublime Text あたりを使ってAngularのコードを書いている人たちです1
何故こんな縛りがあるかというと、これらのエディタは共通して tsserver2というTypeScriptにバンドルされた言語サポートサービスを利用しており、今回はこいつに侵襲する話だからです。
思い返すと2015年の今頃もTypeScriptを魔改造してjspmのモジュールをimport出来るようにする等という記事を書いていました。僕は年の瀬になるとTypeScriptを改造したくなる性癖があるようです。

インストール方法

中身の仕組みは後述しますが、時間や興味があまり無い人のために適用手順だけ貼っておきます。

https://github.com/Quramy/ng-tsserver

  1. ターミナルを開いて(angular-cli等で作った)プロジェクトのルートディレクトリに移動
  2. npm install typescript @angular/language-service reflect-metadataで必要なNPMパッケージをインストール
  3. 以下のコマンドを実行
curl 'https://raw.githubusercontent.com/Quramy/ng-tsserver/master/ng-tsserver'| bash

これで2.でインストールされたtsserverにパッチがあたり、Angularのエディタサポートが追加されます。
大概のTypeScriptエディタプラグインは、tsserverの場所を設定で指定できるようになっているので、それぞれのエディタ設定でローカルインストールされているtsserverを利用するように設定してください3

どのように実現しているか

ここからは、今回の機能をどのように実装したかの自慢話です。

まず、Angular向けの言語サポート機能については、@angular/language-serviceというNPMパッケージを利用しています。@angularと付与されていることからも分かるように、Angularコアチーム謹製で、2.3系から追加されました。ちなみにchuckjazさんが制作者です。主にCompiler周りを作ってる方ですね。

ところでこのモジュール、Angularに興味があってかつエディタのプラグイン製作をやってる人間しか直接触ることが無いという事情のためか4、READMEすらない有様です。
使い方が全く分からなかったので、適当に.d.tsを読み漁っていると、下記を見つけました。

ts_plugin.d.ts
import*astsfrom'typescript';/** A plugin to TypeScript's langauge service that provide language services for * templates in string literals. * * @experimental */exportdeclareclassLanguageServicePlugin{privateserviceHost;privateservice;privatehost;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をひも解くと以下の記述があります。

typescript.d.ts
namespacets{// 一部抜粋functioncreateLanguageService(host: LanguageServiceHost,documentRegistry?: DocumentRegistry):LanguageService;interfaceLanguageService{getSyntacticDiagnostics(fileName: string):Diagnostic[];getCompletionsAtPosition(fileName: string,position: number):CompletionInfo;// 他にも色々メソッドが生えている}}

ということで、次のプランで進めればtsserverにAngular LanguageServiceを統合できそうです。

  1. TypeScriptのcreateLanguageServiceを乗っとっる
  2. オリジナルの createLanguageServiceで作成したLanguageServiceに対して、AngularのLanguageServiceが持つメソッドでラップ
  3. tsserverに2.でラップしたcreateLanguageServiceを利用させる

さて、問題となるのは3.の部分です。TypeScript本体をforkして自前でビルドする、という手段も考えたのですが、他の人に使ってもらうには厳しいやり方です。
また、tsserverはrequireした途端に起動する挙句に、外部からオプションが渡せるような仕組みも特にありません。
一応、tsserverlibrary.jsというのもTypeScriptに同梱されているのですが、こいつは肝心のserver部分を自分で実装しないと使えない代物なので今回は却下。

TypeScript本体の.jsは、全て tsという名前空間(内部モジュール)の中に実装されているので、事前に ts.createLanguageServiceに Property Descriptorを仕込んでおくという手段を用いました。

constNgLanguageServicePlugin=require("@angular/language-service")().default;if(typeofts==="undefined")ts={};vardelegate;Object.defineProperty(ts,"createLanguageService",{get:function(){returnfunction(host,documentRegistry){constls=delegate(host,documentRegistry);constnglsp=newNgLanguageServicePlugin({host:host,service:ls,registry:documentRegistry});// オリジナルのメソッドを退避constcompletionFn=ls.getCompletionsAtPosition;constsamnticCheckFn=ls.getSemanticDiagnostics;// LanguageServiceのメソッドをラップls.getCompletionsAtPosition=(filename,position)=>{constngResult=nglsp.getCompletionsAtPosition(filename,position);if(ngResult)returnngResult;returncompletionFn(filename,position);};ls.getSemanticDiagnostics=(fileName)=>{returnnglsp.getSemanticDiagnosticsFilter(fileName,samnticCheckFn(fileName));};returnls;}},set:function(v){delegate=v;},configurable:true,enumerable:true});varts;// tsserver.jsに定義されてる

このようにしておくと、tsserverの実装の中で、ts.createLanguageServiceが呼びだされる箇所では、上記のgetterが動作して、ラップ済みのLanguageServiceが返却されるようになります。
インストール手順で実行したシェルスクリプトは、上記の.jsに対して、インストール済みのtsserver.jsをappendして置き換えるだけの仕組みということです。

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

おわりに

今回は、AngularのLanguageServiceをTypeScriptのLanguageServiceに統合してエディタから利用可能にする方法について記載しました。

AngularのLanguageServiceはAoTコンパイルと同様に、@angular/compilerをオフラインで実行する機能です。
以前にAngular AoTガイドにて、次のように書きました。

上述したように、ngcコマンドを利用するとHTMLテンプレートから.ngfactory.tsを出力します。bundleを作成する際にはこの.ngfactory.tsをJavaScriptにトランスパイルし、モジュールバンドラでbundleファイルを作成する、というビルドフローが発生します。
言い換えると、ビルドフローの中でHTMLテンプレート(に相当するTypeScript)に対して型がチェックされるという意味になります。

エディタ + LanguageServiceを使えば、ビルドよりもさらに早いタイミングでテンプレートのチェックが可能になるわけですから、利用しない手は無いですね!

正直、node_modules配下の.jsに手を突っ込むというのが相当キワモノであることは自覚しているので、より良い統合方法を知っている方がいたら、コメントやPRで教えてくれると嬉しいです。

脚注


  1. 少し古いですが、 http://angularjs.blogspot.jp/2015/09/angular-2-survey-results.htmlを見るとVimやSublime勢がそれなりの比率で存在していることが分かります 

  2. http://qiita.com/Quramy/items/c2835bc380c3e822d479 

  3. Tsuquyomi(Vim)の場合、デフォルトでローカルインストールされたtsserverを利用するので設定不要。tide(Emacs)の場合は、https://github.com/ananthakumaran/tide#faqを参照のこと。 

  4. 多分、世界でも10人程度しか興味を持ってる人がいないんじゃないだろうか。 


Viewing all articles
Browse latest Browse all 5608

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>