More than 1 year has passed since last update.

AngularJSロゴ

会社でAngularJSをわっしわっし使ってみて、そろそろ某かの意見を言えるようになったのではないか?と思ったので、推薦記事を書いてみます。AngularJSマンセー!

一般のAngularJS解説記事はまぁ巷にそれなりに出回っていると思うので、僕の周りに蔓延る混沌Andoroidクラスタ向けの説明記事を書いてみようと思います。
というわけで、AngularJSをAndroid用語を駆使して説明します。正確さはあまり気にしません。

読者に要求されるもの

  • Androidについての基本的な知識
  • JavaScriptやHTMLなどに対する基本的な知識

参考

わかめのAngularJSはてブ

AngularJSってなに?

AngularJSはGoogleが作成しているWebアプリのクライアント側用フレームワークです。
遠い仲間としてBackbone.jsが、近い仲間としてKnockout.jsFlightがあります。
(まぁわかめはこの中ではBackbone.jsしか使ったことがないんですが。)

近年のWebアプリ界隈の発展と注文の増加っぷりは目覚ましく、JavaScriptで実装する対象がどんどん拡大しているといえます。静的ページで対応できる要素なんてタカが知れているんだ…!
要するに、要求がリッチになってきているので、クライアント側も発展しないとつらぽよだということです。
そこでJavaScript用のフレームワークだ!フリーダムすぎるJavaScriptで徒手空拳とか全裸と絆創膏だけでシベリアに遊びに行くようなもんですよ。

なぜAngularJSを選ぶべきなのか?

サーバ側動的ページ生成とか古いですよ!

ぶっちゃけ、今更新規にアプリ作るのにJSPとか使って、何か操作するたびサーバ側でページ生成して…みたいな作り方することってないですよね。サーバはJSON吐く機械。ユーザに対する見た目(UI)を組み立て制御するのはJavaScriptでやるのが定番です。

JavaScriptで何かするにはフレームワークが欲しいですよ!

JavaScriptは大変自由に書くことのできる言語で一定のルールを設けずに書くとなかなかヒドイことになります。わかめがTG社に入社したばかりの約3年前は、高階関数っぽい書き方をした中二病感漂うヤツと、ちゃんとOOPっぽく書くヤツと、C言語のように構造体+関数で書くヤツが混在してたような記憶があります。それはもう酷い有様です。(俺がどれかはあえて述べない)
そこで、何らかのフレームワークを与え定まった書き方ができるのはメリットです。これはAngularJSに限った話ではないですが。

pushStateを自然に使うのが簡単になるよ!

githubでは、リポジトリ内で移動する時にはページを読みなおして移動するのではなく、必要な部分のみ更新していて、大変シームレスに移動することができます。なおかつ、ブラウザの戻るや進むもキチンと使えます。この機能をしっかり提供するためには、あるページが読み込まれた時に現在のURLから適切にページをJavaScriptで構築しなければなりません。これを自分で1から作るのは結構な苦労です。
AngularJSであれば、URLに対してどういう画面を表示するかを指定する簡単な方法が提供されています。

テストを書くのが楽だよ!

あとはAngularJSはテストが楽なのも好ポイントです。DI(Dependency Injection 依存性注入)をベースに色々な部品間でデータの受け渡しをしているので、テストの時にはMockを利用するようにするのも簡単です。このお陰でAjaxが絡んだテストもやりやすくなっています。

プログラムとDOMの分離が行いやすいよ!

さらに、通常のWebアプリではjQueryを使ってDOMをごしごし組み立てて追加して…といったように、見た目の操作にはプログラミングの知識が必須でした。ですが、AngularJSではDOMをプログラムからいじる場面が大変少なくなるので、プログラマの手を借りずにデザイナさんが独力でいじくれる範囲が他のフレームワークより広くなるのではないかと思います。想像ですが。

つまり

今までわかめが試した限りでは、画面内でのDOMの組み換えや動きなどの少ないアプリ、例えば業務系のアプリなどを手早く組み立てるのに向いているように思います。

そしてAngularJSを選ぶと苦労しそうな例

逆に、AngularJSを使うと苦労しそうなアプリはどういうアプリかを考えてみます。申し訳ないですが、まだ説明していない用語も使って書くのでこの記事を最後まで読み終わった後にもう一度読むとよくわかるかもしれません。

既存のアプリを後からAngularJS化するのはちょっと…

まず、既存のアプリをAngularJSに当てはめるのはあまりオススメできません。AngularJSに限らずですが、フレームワークというのはその世界観に100%入り浸った時に最大の恩恵が受けられるものです。なので、アプリを開発する時にAngularJSを導入する!と決めてしまうのが一番良いでしょう。無理矢理途中からAngularJSに置き換えた場合、controllerとDOMとの結合が強すぎて上手くテストできない!とかデータ通信周りのテストがあああぁ!とか$routeProviderを使った振り分けがあぁぁぁ!みたいな問題が出てきそうです。

DOMをぐりぐりいじくり倒すようなアプリはちょっと…

また、AngularJSではservicecontrollerではDOMをいじくるようなことはしてはいけない事になっています。なので、directiveを作成してDOMを操作するしかない…んですが、それがなかなかメンドクサイ。わかめは未だに作り方がピンと来ていません。逆に言うと、見た目や挙動に凝らなければDOM操作はかなり避けて作れる、ということでもあります。
そのため、例えばゲームのようなアプリや、DOMをぐりぐりいじくり回すようなアプリは苦手なのではないかと思います。

と、思ったら1000人対戦ボンバーマンのBombermineがどうやらAngularJS使ってるみたいなんですよね…ど、どういうことだってばよ…( ゚д゚)

AngularJSのコードを見てみる

HTML+AngularJSしてみる

サンプル を開くと自分で編集して試すことができます。jsFiddle便利なので、海外では結構流行っているみたいです。

まず最初のサンプルです。最初はHTMLを書くだけで出来る範囲で見てみましょう。
以下の抜き出しではjsFiddleと少し変えて、より普通のAngularJSっぽくしてあります。

sample.html
<!DOCTYPE html>
<html ng-app>
<body>
    <input type="text" ng-model="data" placeholder="名前?">
    <hr>
    {{data || 'vvakame'}} より愛をこめて
</body>
</html>

ng-app属性が付いているものがAngularJSの管理下になります。これはHTML中に通常1回だけ出てきます。
通常、ng- で始まる属性はAngularJSがデフォルトで用意している要素です。つまり、公式のAPIを見れば解説が載っています。ng-appについて知りたい場合はngAppで検索したほうが良いかもしれません。

次に出てくるのはng-modelです。ng-modelの中にはAngular Expressionsと呼ばれる式を書くことができます。概ねJavaScriptと似たような構文です。ここでは、dataという変数を参照しています。inputタグへの入力値がdataに反映されるようになります。
その後にある{{}}の中もAngular Expressionsで、data || 'vvakame'が評価され、dataの中身が表示されます。dataが空の場合は後者の'vvakame'が表示されます。

レッスン!
試しに、"○○から××へ愛をこめて" の形式になるように遊んでみましょう。dataの部分をfromにし、もう一つtoのinputも増やしてみると上手くいきそうです。

HTML+JavaScript+AngularJSしてみる

サンプル

2番目のサンプルです。ちょっとJavaScriptを書いてみます。controllerを導入してみます。
ここでは function Controller($scope) という関数を定義し、HTML側でng-controllerに関数名を指定しています。引数の$scopeはAngularJSではよく見る名前の引数です。$scopeにセットした内容は、HTML側から参照することができます。変数のスコープまるまるそのものですね。
入れ子にした場合どうなるとか、$rootScopeと呼ばれる一番上位のスコープがあったりするのですが、この記事では割愛します。詳しくはこちらを参照してください。
AngularJSのDIは、名前に従ってDIされるので、$scope以外の名前にすると上手く動かなくなってしまいます。どういう原理でDIをしているかというと、関数をtoStringすると関数のソースが手に入るので正規表現で無理矢理仮引数の名前を解析してDIしてます。無茶苦茶です。その場合、minifyすると引数の名前が変わってぶっ壊れちゃうんですが、そのための書き方もしっかり用意されています。しかし、めんどくさいので本記事では取り上げません。(というか、あんまり研究してない)

sample.html
<div ng-app>
    <div ng-controller="Controller">
        <input type="text" ng-model="data" placeholder="名前?">
        <button ng-click="reset()">消す</button>
        <hr>
        {{data}} より愛をこめて
    </div>
</div>
sample.js
function Controller($scope) {
    $scope.data = "vvakame";
    $scope.reset = function () {
        $scope.data = "謎の誰か";
    };
}

ng-controllerは、そのタグの配下を制御するcontrollerを指定してやります。controllerには関数の名前を指定します。Controller内部では、変数の初期設定や使える関数を定義しています。$scope.resetではdataの値を書き換えています。そして、ng-clickの中ではその関数を呼んでいます。

AngularJSをAndroidで説明する

では、AngularJSで出てくる主要な仕組みや要素についてこれから説明していきます。

module = AndroidManifest.xml や library project 相当

AngularJSで何かをする時に、moduleを作成することを避けて通ることはできません。
サンプルコードだとmoduleを作っていない場合も多いですが、moduleはAndroidManifest.xmlやlibrary projectに相当する設定が書けるのでざくざく書いてみましょう。

サンプル

sample.js
angular.module("sample", [], function() {
    alert("初期化!");
});

function Controller($scope) {
    $scope.data = "vvakame";
    $scope.reset = function () {
        $scope.data = "謎の誰か";
    };
}
sample.html
<div ng-app="sample">
    <div ng-controller="Controller">
        <input type="text" ng-model="data" placeholder="名前?">
        <button ng-click="reset()">消す</button>
        <hr>
        {{data}} より愛をこめて
    </div>
</div>

このようにします。moduleを定義した後は、ng-appにmodule名を指定すると、そのmoduleの元で初期化を行います。
この例だと、どっちかというとカスタムしたandroid.app.Applicationっぽいですねw

module で行うことが多いのは以下の設定です。

依存するmoduleの指定

ざっくり例えると、使うlibrary projectを追加する感じ。例えばInAppBillingが使いたい時はlibrary projectを使ったりするように、他の人が作ったmoduleや自作のmoduleを追加することもできます。

サンプル

$routeProvider の設定

ざっくり例えると、Activityとそれに対するIntentFilterを書くのに近いです。どのURLが表示/遷移された時にどのを表示するかを指定します。

サンプル pushStateを使うので、iframeだとなんか上手く動かないぽいのでjsFiddleは無しです。 gist

sample.js
angular.module("myApp", [], function ($routeProvider, $locationProvider) {
    $routeProvider
        .when("/foo", {
            templateUrl: "/template/foo.html"
        })
        .when("/bar", {
            templateUrl: "/template/bar.html"
        })
        .otherwise({
            templateUrl: "/template/main.html"
        });
    $locationProvider.html5Mode(true);
});
sample.html
<!DOCTYPE html>
<html ng-app="myApp">
<head>
    <title>AngularJS + $rootProvider の説明</title>

    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

    <script type="text/javascript" src="jquery.js"></script>
    <script type="text/javascript" src="angular.js"></script>
    <script type="text/javascript" src="main.js"></script>
</head>
<body>

<marquee>こんにちは〜</marquee>

<div ng-view></div>

<script type="text/ng-template" id="/template/foo.html">
    ふー!<br>
    <a ng-href="/u/6581286/sample/AngularJS-routeProvider/index.html">メイン</a><br>
    <a ng-href="/bar">ばー!</a>
</script>
<script type="text/ng-template" id="/template/bar.html">
    ばー!<br>
    <a ng-href="/u/6581286/sample/AngularJS-routeProvider/index.html">メイン</a><br>
    <a ng-href="/foo">ふー!</a>
</script>
<script type="text/ng-template" id="/template/main.html">
    おいでませクッパ城<br>
    <a ng-href="/foo">ふー!</a><br>
    <a ng-href="/bar">ばー!</a>
</script>

</body>
</html>

こんな感じです。リンクをクリックすると、ページの内容が差し替えられ、URLも書き換えられます。
$rootProviderを使って、どのURLのパターンの時にng-viewに何を当てはめるかを指定してやります。この例では見た目を差し替えているだけですが、通常はcontrollerの指定をおこなったりもします。
実際使う時は、URLのパターンに/resource/:resourceIdなどと指定すると、$routeParamsをDIで受け取った時に$routeParams.resourceIdなどで値を利用することもできます。RESTっぽい設計にするとき大変便利ですね。

directive の設定

ざっくり例えると、独自Viewを作成するだけしておいてlibrary projectに投げ込んでおく感じです。実際に使うのはまた別の話。

service の設定

ざっくり例えると、自作のSystemServiceを作成して登録する感じです。AndroidでいうService(バックグラウンドでほげほげしたい!)とは全く別のものです。どっちかというとContentProviderみたいなデータをやり取りするための口、に近いです。

filter の作成

ざっくり例えようにも、Androidにはあんまり該当するものがないんですよね…。android.widget.FilterやAdapterが近いかもしれないけど、もっと柔軟で汎用的な仕組みです。Arrayから該当する要素だけを抜き出したり、JSONに変換したり、色々なことが出来るので、フィルタというよりコンバータの方が適切な言い方かもしれないですね。
紛らわしいことに、filterというfilterが存在して、これはまんまArrayから条件に当てはまる要素のみを抽出することができます。

directive = 独自View

directiveは、HTML上で見慣れないタグや属性があったらもう全部directiveって事でいいんじゃないでしょうか?
ng-appng-modelng-clickもアレもコレも実は全部directiveとして実装されているんだよ!ナ、ナンダッテー!!

AngularJSでは、DOMを明示的にいじくる事はほとんどありません。むしろ、テストのやりやすさのために後述のservicecontroller中ではjQueryなどを使ってDOMをいじくるのは積極的に非推奨です。可能な限り行わないようにしましょう。
では、DOMをいじくりたい時、jQueryのDatePickerやGoogle MapsのWidgetを使いたい時、どうやってAngularJSの世界に組み込めば良いのか?そういうのはdirectiveを使って定義することになります。
AngularUIなどのサンプルをみてみると大変分かりやすいです。既存のjQuery pluginとかをAngularJSに組み込む時のサンプルにもなるでしょう。

サンプル

sample.js
angular.module('myApp', [])
    .directive('vvMarquee', function ($parse) {
        return {
            restrict: 'A',
            require: ['?ngModel'],
            replace: true,
            template: '<marquee/>',
            link: function (scope, element, attrs) {
                scope.$watch(attrs.ngModel, function (newVal) {
                    var getter = $parse(attrs.ngModel);
                    var model = getter(scope);

                    element.html(model);
                });
            }
        };
    });

function Controller($scope) {
    $scope.data = "vvakame";
}
sample.html
<div ng-app="myApp" ng-controller="Controller">
    <div vv-marquee ng-model="data"></div>
    <input type="text" ng-model="data">
</div>

このような感じで、DOMをいじくったりできます。ここでは昔なつかしmarqueeタグを使ってみました。ここでは詳しい説明を省きます。ぶっちゃけ難しくてキッチリ説明できるほどわかめも理解できていないという説が有力です。

もうちょいかっこいい例

service = Context#getSystemService(name)的な

serviceは、わかめの理解ではサーバとAngularJSの世界を繋ぐためのノリのように使うのが良いようです。SystemServiceみたいとは一体なんだったのか…。事前にServiceの定義を用意しておいて、Fragmentなどから自在に呼び出してコキ使う…というのに向いています。
こうしておくと、AngularJSはDIをベースに成り立っているため、サーバとの接続部分だけをテストしたり、controllerがデータを受け取って処理をしたり…という部分を個別にテストしやすくなり、テスタビリティが向上します。

サンプル

sample.js
angular.module("twitter", [], function () {
})
.factory("twitterService", function ($http) {
    return new TwitterService($http);
});

function TwitterService($http) {
    this.getList = function (word) {
        return $http({
            method: "JSONP",
            url: "http://search.twitter.com/search.json?q=" + word + "&callback=JSON_CALLBACK"
        });
    };
}

function Controller($scope, twitterService) {
    twitterService.getList("AngularJS").success(function(data) {
        $scope.tweets = data.results;
    });
};

このような感じで、twitterServiceをserviceとして登録し、Controllerから利用しています。$httpの使い方の説明はめんどくさいので説明を省きますが、TwitterのSearch APIでツイートを検索し、取得しています。ここで、serviceを利用することでControllerからはAjaxを利用していることなどを隠蔽し、普通のメソッド呼び出しの体になっています。
TypeScriptを利用していることもあって、わかめはこの形式でserviceを利用しています。

filter = android.widget.Filter or Adapter

filterは、主にデータの加工に利用したりします。Androidではありませんが、例えば cat hoge.tx | grep android | wc -l というコマンドを考えると、cat hoge.txt部分が加工元のデータ、grep androidが1つ目のfilter、wc -lが2つ目のfilter、といった具合です。

サンプル

sample.js
angular.module("myApp", [])
    .filter("upper", function() {
        return function(input, options) {
            return input.map(function(data) {
                return {
                    id: data.id,
                    name: data.name.toUpperCase()
                };
            });
        };
    });

function Controller($scope) {
    $scope.list = [
        {
            id:1,
            name:"vvakame"
        },
        {
            id:2,
            name:"grapswiz"
        },
        {
            id:3,
            name:"u1aryz"
        },
        {
            id:4,
            name:"eaglesakura"
        }
    ];
};
sample.html
<div ng-app="myApp" ng-controller="Controller">
    <input type="text" ng-model="search" placeholder="絞込み">
    <ul>
        <li ng-repeat="data in list | filter:search | upper">
            No.{{data.id}} {{data.name}}
        </li>
    </ul>
</div>

ここでは、AngularJSがデフォルトで持っているfilter filterで絞込みを行った後、作ったupper filterで名前を大文字に変換しています。

controller = Activity or Fragment or Adapter

controllerは、ActivityやFragmentにあたるもので、Viewに対して表示するデータをやりくりしてListFragment#setAdapterにAdapterをセットしてやる、的な感じです。controllerではAdapterなどの代わりに$scopeを利用します。$scopeに対して行った操作は、Viewとの双方向のデータのやり取りに利用します。

また、ネストさせる事もできるので、このように他のControllerの一部を再利用することもできます。ただ、あまり分かりやすいコードでもないですし、使うチャンスは少ないように思います。

サンプル

sample.js
function SelectController($scope, $window) {
    $scope.selectedStateList = [];
    $scope.selectedDataList = [];
    $scope.changeSelection = function(index, data) {
        if ($scope.selectedStateList[index]) {
            $scope.selectedDataList[index] = data;
        } else {
            $scope.selectedDataList[index] = null;
        }
    };
    $scope.show = function() {
        var message = "selected " + $scope.selectedDataList.join(",");
        $window.alert(message);
        $scope.message = message
    };
}

function UserController($scope) {
    $scope.userList = [
        "vvakame", "sys1yagi", "kojira", "neco"
    ];
}
sample.html
<div ng-app>
    <div ng-controller="UserController">
        ユーザ一覧
        <div ng-repeat="user in userList">
            {{user}}
        </div>
    </div>

    <div ng-controller="SelectController">
        ユーザ選択
        <div ng-controller="UserController">
            <div ng-repeat="user in userList">
                <input
                    type="checkbox" 
                    ng-model="selectedStateList[$index]" 
                    ng-change="changeSelection($index, user)">
                {{user}}
            </div>
            {{message}}<br>
            <button ng-click="show()">選択してるユーザを表示</button>
        </div>
    </div>
</div>

HTML = layout xml

HTMLはまんまHTMLの本来の役割通りの画面です。directiveと同じく、だいたいViewだと思ってもよいでしょう。HTMLはcontrollerからデータを与えてもらい、画面に反映する役割を持ちます。どういう構造で表示するかを書く、という面でlayout xmlと似ています。ですが、どのcontrollerが制御するかなどを指定する役割もあるので、細かく違うところももちろんあります。

HTMLについてもここまでに色々とサンプルで出てきたのでもはや割愛します。

$window や $location とか

unittestを行う際に実際のwindowやlocationなどを使ってしまうと、node.js環境で存在しなかったり、window.alertが呼ばれた時にポップアップが出てウザかったりするのでDIされた物を使うようにします。そのためにこれらが準備されています。

AngularJSのもう少し詳しい情報

他に知ってたほうがいいことを概説してみます。

  • Form 入力値のvalidationやエラーメッセージの表示なども手軽に行うことができます。
  • Expression AngularJSでdirectiveなどに指定するExpressionについての解説です。JavaScriptっぽい構文ですが、完全に別物なのでうんうん唸って苦しんだりすることになります。なかなか覚えられなくて辛いです。
  • UnitTest 上でくどくどと言っていますが、AngularJSはDIで成り立っているので、テストもなかなか書きやすいです。ルールが飲み込めてくるまでは結構苦労するんですが…。Jasmineとかで適当にテストを書くことができます。ていうかこのドキュメントの最下部なんなの?マジぱないっす。
    • testacular そのうえで、よく使われているのがtestacularです。AngularJS自体もtestacularを利用してテストされています。PhantomJSやChrome、Firefox、Safari、IEなど複数のブラウザ上でテストを一括で走らせることができます。
  • E2E Test End to End testを書くこともできます。結構こういうのって珍しい気がします。この辺りはまだちゃんと研究していないのでshogoggのブログとかを参照してみるといいかもしれない。

AngularJSとあまり関係のない情報

  • TypeScript 型があるぞー!!JavaScriptとの互換性もかなりあるぞー!!やったーー!!!
  • tsd TypeScript用の.d.tsファイルのダウンローダ的な感じ。
  • grunt 色々なタスクを行わせることができます。shell script書くより、色々な人が作ったタスクを組み合わせたほうが楽です。JavaScriptで設定ファイル書けますし。どっかのm○nとかいうXML編集させられるクソツールよりいいと思います。
  • bower npmのクライアント版です。

参考情報 という名の愚痴

本家ドキュメント

ここ ぶっちゃけ、ドキュメントとしては量が少ないです。かなり大きなフレームワークですがそれに対して量が十分ではありません。なので、AngularJSを使い始めて、その世界観に慣れるまでに結構時間がかかります。わかめもなんやかやで2週間くらいかかった気がします。

また、本家ドキュメントとは直接関係はないのですが、Googleとかで検索した結果の資料として、1.0以前の内容はアテにならない場合が多いため、昔の情報はあまり信用しないほうがいいかもしれません。

テストの書き方

わかるまでが大変なのが、serviceやcontrollerのテストです。
そろそろサンプル作成するのが大変になってきたので、今書いているプロジェクトから抜粋します。言語はTypeScriptです。ご容赦ください…!!
普段はTypeScript+Jasmine+Testacularでテストを作って流して、ってしています。

test.ts
describe("Serviceの", ()=> {
    var $injector:ng.auto.IInjectorService;
    beforeEach(()=> {
        $injector = angular.injector(['ngMock']);
    });

    describe("Sample.Serviceの", ()=> {
        var $httpBackend:ng.IHttpBackendService;
        var service:Sample.Service;

        beforeEach(()=> {
            $httpBackend = $injector.get("$httpBackend");

            service = $injector.instantiate(Sample.Service, {
                $routeParams: {
                    hoge: "fuga"
                }
            });
        });

        it("getメソッドのテスト", ()=> {
            $httpBackend.expect("POST", null).respond(200, {"hoge": "fuga"});
            var promise = service.get();

            var model;
            promise.success((data)=> model = data);
            $httpBackend.flush();

            expect(model).toBeDefined();
        });
    });
});

describe("Controllerの", ()=> {
    var $injector:ng.auto.IInjectorService;
    beforeEach(()=> {
        $injector = angular.injector(['ngMock']);
    });

    describe("Sample.Controllerの", ()=> {
        var $scope:Sample.Scope;
        var $controller:ng.IControllerService;
        var $httpBackend:ng.IHttpBackendService;

        var locals:any;
        var makeController = (specificLocals:any = locals):Sample.Controller => {
            return $controller(Sample.Controller, specificLocals);
        };

        beforeEach(()=> {
            $httpBackend = $injector.get("$httpBackend");
            $controller = $injector.get("$controller");

            $scope = <any> $injector.get("$rootScope").$new();

            var sampleService = $injector.instantiate(Sample.Service, {});
            locals = {
                $scope: $scope,
                sampleService: sampleService
            };
        });

        it("Controllerの作成", ()=> {
            var controller = makeController();

            expect(controller).not.toBeNull();
        });

        it("actionメソッドのテスト", ()=> {
            var controller = makeController();

            $httpBackend.expect("POST", null).respond(200, {"hoge": "fuga"});
            $scope.action();
            $httpBackend.flush();

            expect($scope.result).toBeTruthy();
        });
    });
});

プロジェクト構成

普段使ってる設定

日本語の情報

AngularJSドキュメント日本語訳プロジェクト
AngularJS メーリングリスト

後半の失速っぷりハンパナイネ!

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.