ぷるぷるした直方体

エンジニアの雑記です。My opinions are my ownというやつです

小さなWebアプリ制作環境としてのAngularDart

ちょっとしたWebアプリを作りたい時、皆さんはどのフレームワークを使いますか?

もちろん「ちょっとした」の要件が多岐にわたるため、一概にコレと言えないかと思いますが、私はcreate-react-appnext.jsをよく使っていました。が、勢いに任せた小さいアプリだからこそサクサク開発したいもの。

個人的には、TypescriptとMaterial Designのコンポーネントがあると開発が断然楽ちんなので是非とも使いたいところ。Typescriptはreact-scripts-tsにすれば解決するのですが、Material Designのライブラリは自分で選んで設定する必要があります。そうこうしている内に、もう自分でテンプレート作ってGitHubに上げるかー、という気持ちが芽生えてしまいます。若かりし日の過ちですね。数か月後、セキュリティの警告がGithub上に表示され、パッケージをアップグレードするとコンフリクトして壊れる……という現象は夏の風物詩と言えるでしょう。

そんなこんなで疲れていたので、別のフレームワークを試してみるのに丁度いい頃合いでした。代替はいくつか(も)ありますが、「型が付いててMaterial Design」と言えばAngularDartが浮かんでくるのは自然な流れですね。FlutterのおかげでDartも今後盛り上がってきそうな感じもありますし。

ということで、以下ではAngularDartを使って↓のカードを生成するWebアプリを作った話、その中で体感した利点や欠点を述べてゆきます。ソースを公開していますので、参照しつつ読むと分かりやすいです。

AngularDartの利点

まえがきでも少し述べていますが、AngularDartの強みとして「デフォルト(+公式)で開発を支える機能を多く備えている」ということが言えるでしょう。特に型が自然に使えるのは個人的に大きなメリットであります。個人的にね!

個人的には、以下の機能が既に備えられたjsプロジェクトを触っている感じでした。

  • TypeScript
  • prettier
  • Material Design
  • SCSS

コード整形が標準で仕込まれているので(Goみたいな感じ)、prettierに相当する設定は必要はありません。Material DesignとSCSSは外部ライブラリで対応ですが、1行追加すれば良いだけです。開発初期に疲弊しなくて済みますね。

Angular自体はあまり深入りしていないので特に比較は書きません。ReactとVue.jsの経験があるためか、特に違和感はありませんでした。stateの管理や大規模なコンポーネントは今回作っていないのですが、機会があれば調査してみようと思います。

AngularDartの欠点

物事には表と裏があり、リスクがあればリワードがあり、利点があれば欠点があるものです。

AngularDartが最も敬遠される理由は「jsではない」ことだと言えるでしょう。jsから離れたからこそデフォルトで備えられた機能がある一方、既存の巨大なエコシステムから離れることはフォロワーの激減と大変な手間を要することを意味します。さらに現時点では(jsと比べると)コミュニティも小さく、それによるデメリットは無視できません。

今回の開発で感じた課題点を以下にまとめます。

  • いくつかの有名ライブラリが使えない
  • デプロイサービスなどが対応していない
  • 検索で解決策が出にくい
  • (現状は)hot-replaceを実現するにはwebpackの力を借りる必要がある

今回はごく小さなプロジェクトなのでそこまで影響は受けませんでしたが、規模が膨らみ人員が増えない個人プロジェクトでは、これらの問題は得てして致命的になります。

製作記 あるいはRTA

さて、ここからは実際にOpen Graphのデータを引っこ抜いて静的なHTMLカードを吐き出すという、何とも用途の限られるWebアプリの製作記を記載します。これを通して、小さなアプリをどれくらい簡単に作れるのか を探りました。時間は図っていませんが、開発というよりRTAだと考える方が適切でしょう。

開始時、Angularの知識は皆無、DartもFlutterでちょこっと触っているくらいです。ReactとVue.jsはそこそこ書いたので、その経験が活きると信じましょう。

準備

最初に大まかな仕様を決めます。これ大事。

「URLを入れてボタンをポチッと押すと、外部サイトのmetaタグを引っこ抜いてHTMLを表示する、というのがメイン機能。また、そのHTMLのプレビューが欲しい。コンポーネントはMaterial Designで」という、ざっくりとした要件を決めました。

各種セットアップはガイドに書かれていますので、これに従います。

次におもむろにWebStormを立ち上げ、最新版かどうかを確認します。これ大事。最新版であれば、ウォーミングアップとして公式のサンプルを動かします。

mainのファイルが9行しか無くてインスタ映えします。

mainファイル

手順としてはgit clone, pub get, webdev serveを叩くだけでHello Angular!が出てきます。

公式ガイドを読む

親切に用意されている学習ガイドを読む……のですが、今回はRTAなのでガイドから基本的な要素を取り出しそこだけ読みます。せっかく用意してくれているArchitectureは今回のチャレンジではスキップです。後ほど読みましょう。また、Displaying Dataはすっ飛ばします。

入力の配置

ガイドの4ページ目くらいは入力についての記述です。今回のアプリに関係あるため、読みつつ作っていきます。

まずはHTMLのinputタグを使って、素朴な入力機能を作ります。keyup.enterなどを使えば、ボタンが無くても基本的な入力フローができますね。

HTTP通信とCORS

メイン機能である、外部からデータを取得する箇所を早いこと作ってしまいます。

このアプリにおける実装で最も厄介なのは、「外部サイトのHTMLをどうやって取得するの?」という点です。アプリはクライアントのブラウザのみで動くため、必ずCORSに引っかかります。今回は公開されているCORS回避用サーバー、Rob--W/cors-anywhereさんを使います。ここのAPIサーバーが落ちる時、それがこのアプリの寿命です。昔はYQLというものがあってだな……という老害発言は控えておきましょう。

GET処理は非常に素直に書けます。もちろん.thenを使っても書けます。Future型を書いた時の安心感といったらないですね。

import 'package:http/http.dart' as http;

Future<String> requestCrossDomain(String url) async {
  final corsGateway = 'https://cors-anywhere.herokuapp.com/' + url;
  final jsonString = await http.get(corsGateway);
  final body = jsonString.body;
  ...

コンポーネント分割

さて、残す機能はプレビューです。ここは独立しているので、コンポーネントの分割に挑んでみましょう。

とは言っても、ファイル分割について読み、mainコンポーネントと同じように書くだけです。一般にsrc/以下に配置するようですね。また、Reactでいうpropsは@Inputをつけて宣言するだけで完了です。

また、@Inputの対象はsetterでも良いのが面白いです。

@Input()
set setHtml(String html) {
  final _htmlValidator = PermissiveNodeValidator();
  querySelector('#html-preview')
      ?.setInnerHtml(html, validator: _htmlValidator);
}

呼び出し元

<preview [setHtml]="result" [width]="previewWidth" [height]="previewHeight"></preview>

このようにすると、呼び出し元コンポーネントのresult変数が変わるたびsetHtmlが呼ばれます。便利。

setInnerHTMLとsanitizer

ここはかなりトリッキーな所で、知らないとまず引っかかります。というか大体のWebアプリでは必要ない機能なので、書いていて虚しいです。

Angularでは、setInnerHTMLなどのセキュリティ上危険な機能はpolicyによって上手く管理されています。デフォルトでは拒否するルールになっており、setしようとした内容が削除されてしまいます。今回のアプリでは全許可しても問題がないため、dart-padのリポジトリで行われているように、PermissiveValidatorを作って許可します。

class PermissiveNodeValidator implements NodeValidator {
  bool allowsElement(Element element) => true;

  bool allowsAttribute(Element element, String attributeName, String value) {
    return true;
  }
}

利用例

final _htmlValidator = PermissiveNodeValidator();
querySelector('#html-preview')
    ?.setInnerHtml(html, validator: _htmlValidator);

CSS Sanitizer

今回はさらにニッチな、ユーザー入力に応じてpreviewコンポーネントのCSSを変更するという処理があります。これまたセキュリティポリシーに引っかかっているため、sanitizerを書いて回避します。

Pipeは使い方の例が少なかったため、Angularでの例をDartに移植しました。

以下のように、まさにパイプとして使うことができます。下記コードでは、template内のcssをそのまま無害ということにして流しています。

@Pipe('safe')
class Safe extends PipeTransform {
  DomSanitizationService sanitizer;
  Safe(this.sanitizer);
  transform(style) {
    return this.sanitizer.bypassSecurityTrustStyle(style);
  }
}

@Component(
  pipes: [Safe],
  template: '''
  <div id="html-preview"
   [style]="'width: ' + width + 'px; height: ' + height + 'px; border: 1px dashed black;' | safe"
  ></div>
  ''',

Material Designへ

さて、ここまでで機能は出来ました。あとは見栄えをいじるだけです。公式パッケージが用意されているので導入は楽ちんです。

angular_componentsの基本的な使い方も公式ガイドにあります。例えばボタンであれば、directiveに追加してタグを書き換えるだけです。

directives: [PreviewComponent, MaterialButtonComponent]
<material-button raised (trigger)="onEnter(box.value)">submit</material-button>

また、material-inputの使い方を知るため、パッケージのドキュメントも参照します。開発中特にお世話になるであろうexampleもあります。

HTMLのinputタグは以下のようにmaterial-inputタグになります。

<material-input
        floatingLabel
        type="number"
        checkPositive
        [(ngModel)]="previewWidth"
        leadingText="width: "
        trailingText="px"
        [rightAlign]="true"
></material-input>

やや忘れがちな点として、「materialInputDirectives をdirectivesに追加する」必要があります。これを忘れると、ngModelが動かず悩むことになります。

また、ngModelChangeの右辺値はクラスに含まれるものでないといけません。

だめ

(ngModelChange)="previewWidth=int.parse(\$event)">

良い

(ngModelChange)="previewWidth=intParser(\$event)">
...
int intParser(String str) => int.parse(str);

submitボタンを含めるため、formを作る必要があります。 「バリデーションが通るまでsubmitボタンを無効化」という良くある処理は、ngControlをform内で指定してあげるだけで完了です。

HTMLの切り出し

templateが膨れてきたのでhtmlファイルに切り出します。特に$などの記号を使っていないのであれば、そのまま別ファイルに切り出して指定するだけです。

templateUrl: 'app_component.html',

WebStormでは、切り出すとフォーマットが効くので整形の手間が省けます。

SCSS

cssで書くと辛い場合が多いので、ちょっとしたアプリでもSCSSを使いたいものです。そんな需要も、sass_builderパッケージのReadmeに従うだけで完了です。なお、loadするのは.scssではなく.cssです。

その昔はtrasnformerを記載する必要があったようですが、今ではパッケージを追加するだけというお手軽仕様です。

最後に

忘れずpubspec.yamlの説明等を更新したり、faviconを追加したり、headタグをいじったりしましょう。

デプロイ

どこにデプロイするかは状況によりますが、このような静的アプリではNetlifyが楽ちんです。独立した記事に書いたので、これの通りにスクリプトファイルを用意すれば完了です。

qiita.com

感想

非常にスムーズに進めることができました。学習コストも思っていたほど高くなく、ある程度他のFWを使ったことがあれば引っかからず開発できるかと思います。今回はライブラリへの依存が少ないため、jsでないデメリットがほぼ顕在化せず、多くのメリットを享受できました。

まとめ

要件として「十分に小さいアプリ」「外部ライブラリをあまり使わない」「見た目はやや気になる」という条件を満たす場合、create-react-appを初めとしたReactプロジェクトやnext.jsを使うよりサクサク開発できると感じました。Dartは今後の発展も期待できますし、積極的に使ってゆきたいと思います。

一方で、規模が大きいアプリを作る際には違った観点が必要であり、デメリットが無視できなくなると考えられます。これは今回の制作からは預かり知れないため、いずれ巡り合わせがあれば体験してみます。