“外部ライブラリもインストール・型解釈できる TypeScript playground を作った”という題で俺得フロントエンド (1) LT 会という勉強会で登壇してきました。スライドはこちらです。
スライドの内容をそのまま書いても意味がないので詳しくはスライドを読んでいただきたいのですが、発表時間の都合・構成力不足で伝えきれなかった部分の補足・補完的なものを書きます。
この playground のより詳細な説明、内部で使ってる技術の説明、この playground に限らず汎用的に活用できそうな技術の話をします。
— TypeScript Playground | The unofficial playground for advanced TypeScript users
↑ を作りました。
「動かないじゃん!」と言われても悲しいので、これをきちんと伝えておきたいです。
ちゃんと使えるものにしたいので、何通りかのユースケース用にサンプルを共有し「なるほどこういうことができるのね」のイメージを掴んでいただきたいです。
types
フィールドを足していますtypes
フィールドを足しています(jest に型ゲーなんてあるのか? という感じですが色々あるアサーションメソッドの補完が効きます)デモをいくつか見ると気づくかもしれませんが、react
のように型定義のないパッケージはインストール不要で、代わりに@types/react
のインストールが必要です。react は型定義を配布していないため、@types のパッケージが必要になります。
一方でパッケージ自体が型定義を配布している query-string や Redux などのパッケージはそれ自体を入れる必要が有ります。
文字にするとややこしいんですが、「普通に開発するときにインストールすること」と同じように使えば動くはずです。
TypeScript の playground といえば、TypeScript 公式の playgroundがまっさきに思い浮かぶと思います。主な機能は以下のとおりです。
“〜をする Conditional Type”みたいな、ちょっとしたクイズとしては十分に活用できますし、TypeScript のようなトランスパイル文化に親しみのない方が具体的にどのような JavaScript が生成されるのかを理解したり、実行して成功体験を少しずつ積んだりと、ひとまず TypeScript に触ってみるものとしてとてもいい作りだと思っています。
ただ、型にフォーカスしたときにやや物足りないと思うことがあります。具体的には以下の問題が挙げられます。
対象ユーザと存在意義が違うので、中途半端に本家に混ぜてどっちつかずになるよりも、とことん自分がほしいものを追求した playground を作ってみよう(その後は実用レベルになってから考えよう)と思いたち制作に至りました。
だいたいはスライドの方に書いたので、書き足りないところ・省略したことについて加筆します。
トーク後に「monaco で〜をするにはどんな API・機能を利用すればいいのか、ドキュメントを見てもいまいちわからない」という話をもらったので補足しますが、特別なことはありません。
API ドキュメントを隅から隅まで読む、目的から Issue を探してコメントを追う、ソースコード読むなどして泥臭く可能性を探り、monaco 公式の playground にて実験して確証を得てます。
公式ドキュメントはこちらです。
公式 playground はこちらです。
ユースケースごとに boilerplate が用意されており、初期コードを書き換えてだんだん挙動を理解していく形になると思います。
なお monaco-editor の playground にはシェア機能がありません。付け足したい。
TypeScript に絞って理解を深めるとっかかりを紹介すると、
monaco.languages.typescript.LanguageServiceDefaults
に TypeScript の LanguageService を操作するための API が生えています
setCompilerOptions
メソッドで tsconfig.json を読み込ませられます
CompilerOptions
を得るに Compiler API のメソッドを理解する必要がありますaddExtraLib
メソッドで「このファルパスにこういう型定義ファイルがある(ことにする)」を LanguageService に通知できます— How to use addExtraLib in Monaco with an external type definition - Stack Overflow
公式 playground と同じく、クエリパラメータにアプリの状態(コードや tsconfig、依存パッケージとそのバージョン etc)をすべて載せて共有できるようにし、ページロード時にクエリパラメータから状態を復元する方式を取りました。
よほど長いコードを書かない限りは愚直に base64 エンコードするだけで十分なのですが、共有可能なデータ量を増やすためにブラウザだけで完結する圧縮・解凍を実現するpakoにたどり着きました。
API の互換性はありませんが、Node.js のzlibモジュールのような感覚で使用できます。
Brotli には対応してなかったため gzip を使用したところ、本アプリにおける圧縮率はだいたい 30%でした。無圧縮よりも 30%ほど多くのデータ(コード)をシェアできるようになります。
実際のコードはこのあたりです。
https://github.com/Leko/type-puzzle/blob/61097601b7b35f92b3f611e164731496ffe1d601/packages/playground/src/lib/share.ts
WebAssembly に port された Brotli 実装のwasm-brotliというモジュールも試してみました。圧縮率は 40%と高かったのですが、gzip でも十分用途に足りていると判断し、利用ユーザ数が多い pako を採用しました。
prettier をブラウザで使うのは非常に簡単で、ドキュメントのとおり書くだけです。
WebWorker のサンプルコードも紹介されておりいたれりつくせりなんですが、Off the main thread を実現するにあたって、素の WebWorker のコードを書くよりも、webpack のworker-loaderを使うよりも、Comlinkを経由して暗黙的に WebWorker を利用するよりも、全力で振り切ってcomlink-loaderを使うアプローチが面白かったので紹介します。
もはや黒魔術の域を超え闇の魔術ではないか? とすら思うのですが、GoogleChromeLabs が開発しているcomlink-loaderという Webpack loader があります。
Comlink を生で使っている間は WebWorker の存在を利用者が認識しないといけないのですが、comlink-loader によって WebWorker の存在がコードからほとんど消失します。
重たい処理の例として、こんなクラスがあったとします。
無駄にループを回してメインスレッドを固めます。引数と戻り値を適当に付けておきました。
この重い処理が例えば圧縮・解凍アルゴリズムだったり、prettier だったりするという前提で適宜重たそうな処理に読み替えてください。
export class SomeHeavyTask {
async run(loopCount: number) {
const startsAt = new Date()
for (let i = 0; i < loopCount; i++);
return new Date().getTime() - startsAt.getTime()
}
}
このSomeHeavyTask
はこのように使います
import { SomeHeavyTask } from './some-heavy-task'
const task = new SomeHeavyTask()
task.run(1_000_000_000).then(spent => console.log(`spent: ${spent}ms`))
この処理を comlink-loader に書き換えるとこうなります。
import SomeHeavyTask from 'comlink-loader!./some-heavy-task'
new SomeHeavyTask()
.then(task => task.run())
.then(spent => console.log(`spent: ${spent}ms`))
Promise<SomeHeavyTask>
にかわるという制約はあり、挙動を理解してないとむしろハマりそうな気もしますが、かなり低コストで Off the main thread が実現できるので、カジュアルに重たい処理は worker に逃がすって戦略を取りやすくなります。
処理が Worker に分けた結果 Code splitting も効くので、「特定用途にしか使ってない、ファイルサイズ的にも処理内容的にもヘビーなライブラリ」は格好の移行対象です。
import ShareWorker from "comlink-loader!./lib/share";
import PrettierWorker from "comlink-loader!./lib/prettier";
余計なテンプレート、グルーコードを増やさず気軽に Off the main thread を推し進められる強い武器(諸刃かもしれない)でした。
スライドにも書いてあるのですが、完成度を上げて本家 playgrond に還元したいと思っています。
私一人のユースケースでは品質的にもコンセプト的にも甘いと思うので、ぜひ使ってみてフィードバックをいただけると幸いです。