余白

https://blog.lacolaco.net/ に移転しました

Angularで巨大なライブラリを動的に読み込む

オリジナルはこちら

medium.com

基本的にコードサンプルなどはオリジナルを参照してください。この記事では込み入った事情の部分だけを日本語で補足します。

tsconfig.jsonの準備

tsconfig.jsonmodule 設定は、TypeScript内で記述したモジュールのimport/exportをどのように解決するかを指定します。 Angular CLIのデフォルトでは module: es2015 を指定しているので、静的な import ... from はそのまま残しますが、import() はサポートしていません。 tsconfig.jsonmodule: esnext を指定すると、import()JavaScriptにそのまま残すようになります。 import() がサポートされたブラウザ上であれば、webpackを通さなくてもそのままブラウザ上でモジュール解決できる状態になっています。

ところがまだ import() はTC39のProposalとしてはStage 3で、未サポートのブラウザが多くあります。 現実的には、webpackを使ってbundleする必要がありますが、webpackはこの import() をwebpackがもつ動的モジュール読み込みの仕組み ( require.ensure ) で置き換えてくれます。 つまり、 import() のpolyfillのように振る舞ってくれます。

webpackを通すことでbundle後のJavaScriptには import ... fromimport() も残らないため、 target: es5 のままトランスパイルしても問題ありません。つまりブラウザ互換性には影響しません。 Promiseがないブラウザではes2015のpolyfillが必要ですが、Angular CLI v7.2からはデフォルトで es2015非サポートなブラウザでだけ自動的に適用されるpolyfillを吐き出すので、我々がes2015のpolyfillについて気にすることはありません。

normalizeCommonJSImport について

これは TypeScriptの import() の型定義でCommonJSとの互換性に問題があるための処置です。 import() では名前付きインポートをサポートしておらず、ES Moduleにおける default export だけをサポートしています。 webpackではその互換性のために、CommonJSで書かれたモジュールを import() でインポートするときには、module.exports オブジェクトを default exportに見立てて、 import() で読み込まれるオブジェクトの default プロパティに格納しています。

TypeScriptの import() は賢いので、 静的にimportしたときに import * as Chart from 'chart.js' で得られる Chart の型と、 import('chart.js').then(result => result) で得られる result の型は同じに扱うのですが、実際は result.defaultChart に相当するので、素直に書くとTypeScriptのコンパイルが通りません。 そのために normalizeCommonJSImport でラップしています。

転職のお知らせ

f:id:lacolaco:20190227232704j:plain

写真は妻が作った雪だるまです。

  • From: 株式会社Kaizen Platform
    • 2/28が最終出社日でした
    • やってたこと
      • Webフロントエンド SPA開発 (React/TypeScript)
      • Schema-first GraphQLによるAPI仕様中心開発の整備
      • UX/UI設計
      • Webパフォーマンス計測、改善
      • その他
  • To: bitbank株式会社
    • 4月から入社します
    • 週4日にしてもらいました
    • やりたいこと
      • ある程度の規模に育ったAngularアプリの開発に関わりたい
      • Angularのエキスパートとして持てる力を尽くしてチームを加速させたい
      • Web技術とブロックチェーンのこれからについて、持論を持てる程度の学びを得たい
        • ついでにマイクロペイメントまで学びを得られたら嬉しい

本件についてのお問い合わせは Kyashの送金メッセージでお願いします。(返答を保証するものではありません)

f:id:lacolaco:20190227235324p:plain

README.mdに動的コンテンツを埋め込む、あるいはImage via Functionというアプローチ

突然ですが、 README.md に動的なコンテンツを埋め込みたいと思ったことはないですか?僕はあります。 具体的には、リポジトリのコントリビューターをREADME.mdに埋め込みたいという願望がありました。

つまりこういうことです。

f:id:lacolaco:20190226224230p:plain

しかし毎回CIなどでREADME.mdを編集するのはセットアップが面倒です。 <contributors-list> みたいなCustom Elementsが使えたらきれいな世界だなあと思ったのですが、肝心のscriptタグが動かないのでそれは無理です。 ということで、頼れるのは 画像 ということになりました。

Image via Function

README.mdに埋め込めて、なおかつ動的なコンテンツを扱えるのは画像のURL展開だけなので、つまりコントリビューターリストを画像化するHTTPエンドポイントを用意し、そのURLをREADME.mdに埋め込めばいいわけですね。同じことはすでにCIサービスのSVGバッジなどで広く行われているので、これ自体は特筆すべきことではないと思います。

問題はコントリビューターリストの画像をどうやって作るかということです。今回はまずコントリビューターリストを表示するAngularアプリケーションを作り、そのスクリーンショットをPuppeteerで撮影する、という流れを実装しました。

f:id:lacolaco:20190226225516p:plain

Angularアプリ

シンプルにAngular CLIで作成し、@octokit/rest を使ってGitHub APIを呼び出しています。単純なアプリケーションです。 これをFirebase Hostingで公開し、Puppeteerで表示できるようにします。

https://contributors-img.firebaseapp.com/repo=angular/angular-ja

f:id:lacolaco:20190226225027p:plain

ポイントとしては、画像化したい要素にIDを付与していることです。このアプリケーションでは後ほど画像化するときに #contributors を指定します。

Puppeteer on Cloud Functions

Cloud Functions for FirebaseではHeadless Chromeを便利に扱えるPuppeteerを実行できます。 Puppeteerを使ってHeadless Chromeを起動して、先ほどのAngularアプリケーションにアクセスし、スクリーンショットを撮影します。

async function renderContributorsImage(repository: string): Promise<Buffer> {
  const browser = await puppeteer.launch(
    isDebug
      ? {}
      : {
          headless: true,
          args: ['--no-sandbox'],
        },
  );
  const page = await browser.newPage();

  await page.goto(
    `https://contributors-img.firebaseapp.com?repo=${repository}`,
  );

  const screenshotTarget = await page.waitForSelector('#contributors');
  await page.waitForResponse(() => true);

  const screenshot = await screenshotTarget.screenshot({
    type: 'png',
    omitBackground: true,
  });
  return await browser.close().then(() => screenshot);
}

ここのポイントは await page.waitForResponse(() => true)GitHubアバター画像のリクエストをすべて待っているところです。 これを待たないとHTMLのレンダリングが終わっただけではまだ画像が空っぽになるからです。

Cache in Cloud Storage

Cloud Functions上でPuppeteerを動かすのはメモリを多く使うし実行時間も長くてお金が結構かかりますし、毎回画像を作るのはパフォーマンスも悪いです。 FirebaseのCloud Storage上にリポジトリごとに画像をキャッシュし、キャッシュがある場合はキャッシュから返します。 Cloud StorageのBucketにExpirationの設定をしているので、24時間経った古いキャッシュは自動的に削除されます。Firebaseコンソール側からはこの設定が見えないんですが、GCPのコンソールからアクセスすると普通に使えます。

f:id:lacolaco:20190226223147p:plain

f:id:lacolaco:20190226223210p:plain

async function _createContributorsImage(repository: string): Promise<Buffer> {
  const cacheId = generateCacheId(repository);
  const cacheFile = bucket.file(cacheId);

  console.log(`Look for a cache...`);
  if (await cacheFile.exists().then(data => data[0])) {
    console.log(`Return from the cache`);
    return cacheFile.download().then(data => data[0]);
  }

  console.log(`Render an image`);
  const image = await renderContributorsImage(repository);

  console.log(`Save new cache`);
  await cacheFile.save(image, {});

  console.log(`Return rendered image`);
  return image;
}

Puppeteerを使うHTTP Trigger Functionの定義

Puppeteerはメモリを多量に使うので、Function定義時にメモリの設定が必要です。1GBあればまあ動きます。 ブラウザの起動、Angularアプリの実行、スクリーンショット撮影と保存まで含めると結構時間がかかるので、タイムアウト時間も30秒にしてあります。

export const createContributorsImage = functions
  .runWith({
    timeoutSeconds: 30,
    memory: '1GB',
  })
  .https.onRequest((request, response) => {
    const repoParam = request.query['repo'];

    if (!repoParam || typeof repoParam !== 'string') {
      response.status(400).send(`'repo' parameter is required.`);
      return;
    }

    _createContributorsImage(repoParam)
      .then(image => {
        response.setHeader('Content-Type', 'image/png');
        response.status(200).send(image);
      })
      .catch(err => {
        console.error(err);
        response.status(500).send(err.toString());
      });
  });

完成!

というわけで画像を展開できる場所であればどこでもコントリビューターリストを表示できるようになりました!

例:

https://github.com/angular/angular-ja/blob/283047c5686cb3957966a671264e4c1d7b32a073/README.md

f:id:lacolaco:20190226224230p:plain

24時間ごとにしか更新されない課題はありますが、それほどリアルタイム性があるデータではないので問題ないでしょう。

課題としてはPuppeteerでGitHubアバター画像をダウンロードするときに外部ネットワークが発生するので、僕のお財布にダメージが大きいところです。しばらく従量課金で動かしますが破産しそうになったらリポジトリホワイトリストを作って運用します。

まとめ

  • Cloud Functionを使って画像を返せば、簡単に動的コンテンツを静的ページに埋め込める
  • Cloud Functions + Puppeteer + Webapp で、慣れてるHTML+CSSで作ったUIを画像化できる
    • 画像処理頑張らなくてもいい

久々に趣味プロらしい趣味プロをしたら楽しかったです。また何か思いついたらやります。

Angularの学習コストは本当に高いのか?

有言実行しなきゃね...

この記事では、「学習コストが高い」と評されがちなAngularについて、本当にその学習コストは高いのかということについて紐解いていきます。

先に言っておきますが、ReactやVueをはじめとする他のフレームワークとの比較はしません。また、なかなか本題に入らない回りくどい文章になる予定なので、予めご了承ください。そして筆者はAngularが大好きです。Angularが好きな人間が書いたポジショントークであることは前提として読んでください。

そもそも学習コストとは何だ?

まずはじめに、「学習コスト」って何だ?っていうところから始めましょう。名前からして、学習に関するコストであることは予想できます。しかし明確な定義はGoogleで1時間ほど検索した限りでは見つからなかったので、自力で考えていくことにします。

コスト(=費用)と呼ばれる以上、そこには経済活動の概念との共通点があるのでしょう。Wikipediaで "費用" について調べてみました。

ja.wikipedia.org

費用(ひよう、英: cost, expense)とは、生産や取引などの経済活動に伴って支払う金銭である。

なるほど、コストとは、何かをおこなうために支払う金銭のことでした。であれば「学習コスト」とは学習をおこなうために支払う金銭ということになります。しかし私たちがプログラミングに関することを学習するときに金銭を払うことはあまり多くありません。代わりに私たちは時間を費やして新しい技術を学習しています。この時間を私たちは「学習コスト」と呼んでいるのでしょう。(プログラミングスクールや有料の教材も増えてきたので、今後は本当に金銭が学習コストになるかもしれません。)

コストを減らしたいのは誰か?

コストといえば「削減」されるものです。限りある予算のなかでやりくりするために、かかる費用はできるだけ抑えたいものです。では「学習コスト」における予算とは何でしょうか。なぜ学習コストは低いほうがよいのでしょうか。

ここには、日本のソフトウェア開発のメインストリームが労働集約型のシステム開発であったことに起因するものがありそうです。このモデルで利益を大きくするには、人件費は安ければ安いほうがよく、開発期間が短いほうが収益マシーンをたくさん回すことができます。毎回システムの要件も変わりますので、従業員が必要なスキルを習得済みとは限りません。そうなれば従業員に教育を施す必要があります。当然、この教育も短期間で完了することが望ましいです。つまり、学ばせる側には学習コストが低い技術を選択するインセンティブが発生しています。

一方で、学ぶ側にとってはどうでしょうか。ここではプログラマーは自身のスキルを伸ばすこと、ひいては開発者としての市場価値を高め収入を上げることを目的としていると仮定します。ある技術の学習コストが低いということは、誰でもすぐに学べるということです。学習コストが低い技術では、市場価値は上がりにくいでしょう。かといって、学習コストの高い技術を習得したところで、仕事がなければこれも市場価値が上がりません。どの技術が金になるか、というのは時間とともに変わっていき、ある種の賭けのような側面もあります。ここで重要なのは一度学んだ技術を次に転用できるか、という観点です。新しい技術が出るたびにゼロから学び直していては時間が足りません。一度払った学習コストを再利用して新しい技術を学ぶことで、複利的にスキルアップしていくのです。つまり、学習コストの単なる高低ではなく、「何を学んだのか」という内訳に意味があるのです。

コストの資産化

コストには2種類あります。費用の発生と資産の購入です。支払った資本が、将来の収益の獲得に貢献するならば、それは費用ではなく資産となります。「一度払った学習コストを再利用して新しい技術を学ぶ」というのは、学習コストを資産計上するということです。学習コストが高くても、それが将来に渡って利益を生む資産となるのなら、長期的な観点からその高いコストを払うことが好ましい場面もあります。

ある技術の学習コスト考えるときには、その内訳を考えてみましょう。そのコストの中で資産になる部分、費用になる部分があるはずです。その技術が時代遅れとなり市場で価値がなくなったとき、そのまま無駄になってしまうならそれは費用です。そうでなく、パラダイムシフトを越えて通用する根幹的な技術や概念を学べたなら、それは資産になるでしょう。これはコストの高低には関係ありません。多くが資産になる高コストな技術もあれば、ほとんどが費用になる低コストな技術もあるでしょう。安物買いの銭失いにならないように、資産化しやすい技術を選ぶことがひとつの指針になるかと思います。

さて、ようやく本題にはいる準備ができました。この記事で述べたいのは、Angularの学習コストです。ここまでの話を踏まえて、Angularの学習コストと、その内訳を考えていきましょう。

Angularの学習コスト(〜入門)

まずはAngularに入門するまでに必要なコストについて見ていきましょう。Angularをはじめるにあたって習得する必要がある技術は、HTML/CSS・TypeScript・Observable(RxJS)です。これに加えて、npmを中心としたエコシステムのWeb開発について基本を理解しておく必要もあるでしょう。

f:id:lacolaco:20190219000741p:plain

さて、これらは費用でしょうか。それとも資産になるでしょうか?

HTML / CSS

これは言うまでもなく資産でしょう。世界中のWebページを支えるWebの基本技術です。Angularを学ぶ以前から資産として持っている人も多いでしょう。AngularではじめてWebを学ぶという人にとっても、学んでおいて損はない技術です。

npmエコシステム

npmコマンド無しにWeb開発することは、いまとなってはほぼ皆無に近いでしょう。それほどにnpmを中心としたエコシステムは発展し、成熟し、定着しています。資産として計上されて当然でしょう。

TypeScript

これも大きな資産となるはずです。ここ2,3年でTypeScriptは広く普及しました。Angularに限らずReactやVue、その他のフレームワークやライブラリでも採用されるケースが増えています。Angularのために学んだTypeScriptの基礎は、別の場所でも活用できるでしょう。

Observable (RxJS)

RxJSそのものは、Angularの他で使うことはないかもしれません。その場合は費用として捉えることになるでしょう。しかしObservableあるいはObserverパターンという概念は様々な場面で応用可能です。また、AndroidiOSといったモバイルアプリの開発においては RxJavaやRxSwiftといったRxファミリーを取り入れている場合もあります。マルチプラットフォームのFlutterで採用されているDartにも、Observableに似たStreamという仕組みが言語標準で組み込まれています。リアクティブなプログラミングパラダイムにおいて頻出するパターンなので、リアクティブプログラミングの入門としてAngularのRxJSを捉えてみれば、資産と見なすこともできるでしょう。

RxJSを除いて、Angularの入門に必要な学習はほぼすべて資産となることがわかりました。はじめてのWeb開発というケースでも、Angularを通じて学んだ技術は将来の利益になるでしょう。無駄にはならないので、悩む時間があったらぜひ入門してみましょう。

Angularの学習コスト(入門〜中級)

入門してからもまだまだ学ぶことはあります。確かにAngularの学習コストは高いのです。Angular特有の要素で言えば、Component/Directive・依存性の注入・Routingなどを学ぶ必要があるでしょう。それに加えて、モダンWeb開発につきものの要素として、状態管理・ユニットテストコンポーネント設計・パフォーマンスなどがあります。

f:id:lacolaco:20190219000754p:plain

当然モダンWeb開発をおこなう上で必要な技術は汎用的な資産になると考えてよいでしょう。ここではAngular特有の要素について詳しく見ていきます。

Component / Directive

AngularのComponentやDirective、Templateの仕組みは他に転用できるものではなく、残念ながらほとんどは費用となるでしょう。ただし、すべてがそうなるわけではありません。ここで注目すべきが Web Componentsです。

AngularのComponentは、Web Components技術をモデルに設計されています。ComponentはCustom Elementのように独自タグをもち、内部のCSSはShadow DOMのようにカプセル化されます。デフォルトではエミュレートされたScoped CSSですが、実際にネイティブのShadow DOMを使うように命令することもできます。

さらに、Angular v7から導入されたAngular Elementsでは、AngularのComponentをCustom Elementsに変換することすら可能になりました。一度Custom Elementsとしてエクスポートされれば、フレームワークを問わずどこででも利用できるプログラムになります。

AngularのComponentはWeb Components技術を学ぶきっかけになりますし、実際にWeb Componentsを活用したアプリケーションを構築するためのツールとして利用することもできる。この点を資産と見なしてAngularに投資するのも間違いではないでしょう。

依存性の注入

Webフロントエンドではマイナーですが、サーバーサイド言語では依存性の注入は一般的に行われている技術です。これはAngularで学んだ依存性の注入が資産になるというよりは、すでに依存性の注入を学んでいる人にとってのハードルを下げることに繋がる面が大きいでしょう。

また、TypeScriptの型情報を利用した宣言的なDIシステムは高く評価されており、Angular以外の場所で使うために模倣した仕組みが作られるケースもでてきました。InversifyJSやNestJSはその一例です。このような面から見ても、まったくの費用であるとは言えません。

github.com

github.com

Routing

AngularのRoutingはフレームワークと強く紐づく部分なので、Angular以外への転用は難しいでしょう。素直に費用と見なして学習します。

入門から中級へスキルアップするにつれて、Angularの学習コストは高くなります。ですが中級まで到達したあとにはアプリケーションについて考える時間が増えることでしょう。コンポーネント設計やテスト設計について思いを巡らせ、スケーラビリティと生産性を向上させる手段としてComponent/Directiveや依存性の注入を活用するフェーズに突入します。Angularが手足のように動き始め、アプリケーションに集中できるようになってきたら、中級者の壁を越えた証です。

Q. 結論: Angularの学習コストは高いのか?

  1. 低くはありません。

アプリケーションを開発するための最短距離だけをたどるならもっと学習コストの低いフレームワークになったでしょう。しかしAngularが選んだのはWeb標準やエコシステムと協調し、未来を見据えたスタックです。そのためには今は寄り道と思えるような学習が必要になりますが、その多くは資産となります。得た資産を元に学習することで更に大きな資産となり、複利的にスキルアップできるようになります。

極論、払ったコストよりも大きなリターンがあるのがわかっていればどれだけ高くてもコストは払えます。ですが現実にはリターンがあるかどうか、不確実性があるから尻込みしてしまいますが、そんなときにはリスクヘッジを考えるべきです。失敗してもある程度の資産が残っていれば全損よりはマシです。そしてAngularは多くの資産を残し、リスクヘッジできる技術です。

また、コミュニティが学習コストを下げることができます。誰かが通った道を、後に続く人は楽に歩けるように知見を残していくことができます。そして自分が学んだ知見を残せば、また未来の誰かを助けられます。オープンなコミュニティの存在は学習コストを評価する上で勘定に含めるべきでしょう。Angular日本ユーザー会はそうした互助の精神でこれまでも、これからも運営されています。Angularを学ぶときにはぜひ参加してみてください。

community.angular.jp

angular.jp

長々とお付き合いいただきありがとうございました。学習コストの高低だけじゃない観点でAngularを評価してくれる人が一人でも増えたら嬉しいです。

AngularにおけるListComponent/ListItemComponentの設計

Notionに書いた。

www.notion.so

余談。Notionのほうが書きやすいし読みやすいんだけど、ブログとしての体裁(シェアしやすいとかそういう)が無いのでどうしようか悩む。 Notionで書いてリンクだけブログに貼っていくのどう思います?