Sparkで単体テストをしてみる
Apache Sparkで単体テストをしてみる
Intelij IDEAでsparkの単体テストを書いてみたのでメモ
build.sbtの設定を変更
まず、build.sbtに以下の設定を追加する。
parallelExecution in Test := false
“build sbt"で複数のテストが同時に動いた場合に発生するSparkContext周りのエラーを防ぐのに必要なようである。
テストを書いてみる
まず、以下のようにcsvをDataFrameとして読み込んでデータを取得するclassのテストを書く場合
package intoroduction.spark.dataframe import org.apache.spark.sql.SQLContext import org.apache.spark.{SparkConf, SparkContext} import org.apache.spark.sql.hive.HiveContext import org.apache.spark.sql.types.DataType._ import org.apache.spark.sql.types.IntegerType case class Dessert(menuId: String, name: String, price: Int, kcal: Int) class DesertFrame(sc: SparkContext, sqlContext: SQLContext, filePath: String) { import sqlContext.implicits._ lazy val dessertDF = { val dessertRDD = sc.textFile(filePath) sc.textFile(filePath) // データフレームとして読み込む dessertRDD.map { record => val splitRecord = record.split(",") val menuId = splitRecord(0) val name = splitRecord(1) val price = splitRecord(2).toInt val kcal = splitRecord(3).toInt Dessert(menuId, name, price, kcal) }.toDF } dessertDF.createOrReplaceTempView("desert_table") def findByMenuId(menuId: String) = { dessertDF.where(dessertDF("menuId") === menuId) } } object DesertFrame { def main(args: Array[String]): Unit ={ val conf = new SparkConf().setAppName("DesertFrame").setMaster("local[*]") val sc = new SparkContext(conf) val sqlContext = new SQLContext(sc) import sqlContext.implicits._ val filePath = "src/test/resources/data/dessert-menu.csv" val desertFrame = new DesertFrame(sc, sqlContext, filePath) val d19DF = desertFrame.findByMenuId("D-19").head print(d19DF) } }
上記のDesertFrameのテストを書く場合は以下のようになる。
package intoroduction.spark.dataframe import org.apache.spark.sql.SQLContext import org.apache.spark.{SparkConf, SparkContext} import org.scalatest.{BeforeAndAfterAll, FunSuite} class DessertFrameTest extends FunSuite with BeforeAndAfterAll{ private var sparkConf: SparkConf = _ private var sc: SparkContext = _ private var sqlContext: SQLContext = _ override def beforeAll() { print("before...") sparkConf = new SparkConf().setAppName("DessertFrameTest").setMaster("local") sc = new SparkContext(sparkConf) sqlContext = new SQLContext(sc) } override def afterAll() { sc.stop() print("after...") } test("dessert_frame"){ val filePath = "src/test/resources/data/dessert-menu.csv" val desertFrame = new DesertFrame(sc, sqlContext, filePath) val d19DF = desertFrame.findByMenuId("D-19").head assert(d19DF.get(0) == "D-19") assert(d19DF.get(1) == "キャラメルロール") assert(d19DF.get(2) == 370) assert(d19DF.get(3) == 230) } }
ここでは"SparkConf().setAppName(“DessertFrameTest”).setMaster(“local”)“と指定しており、ローカルの環境で動かすことができるようになりテストで使うデータを"src/test/resources/data/dessert-menu.csv"にしているのでテストデータもそのままgitで管理できるようになっている。
テスト実行
あとは"sbt test"か"sbt test:testOnly クラス指定"でテストを実行できるはずである。
Hadoopについて調べてクラスタを構築してみた
並列分散処理入門
Hadoop,Spark周りについて調べたことをまとめてみる
並列分散処理とは
並列分散処理とは複数のサーバを同時に動かしてデータを処理することである。ビッグデータと呼ばれるような大量のデータを扱う場合はたくさんのサーバを使ってクラスターを構成し、多ければ数千台のサーバを使うということもある。
並列分散処理ツール登場の背景
既存のRDBMSがある中でHadoopなどの代表的な並列分散処理ツールが登場した背景として、RDBMSではトランザクションやACIDなど複雑なデータに不整合が起きないような設計によりそのまま複数台の端末で動かそうとするのであればディスクI/Oの方がボトルネックとなりCPU資源を使い切れず速度が出し切れなくなる問題があった。この問題を解決するためHadoopなどの並列分散処理ツールではRDBMSにあったデータに不整合が発生しないように気にする機能を除外し、Mapreduceという仕組みにより複数台の端末で同時に処理してもディスクI/Oがボトルネックとならないように作られている。
Hadoopについて
概要
Hadoopとは代表的な並列分散処理ツールでGoogleのDoug cuttingにより作られた。Hadoopの名前の由来はDougの子供が持っていたゾウのぬいぐるみ。HadoopではHDFS(Hadoop Distributed File System)により複数のサーバにリソースを分散させて、Mapreduceにより複数の端末で処理を実行するということができるようになっている。
オープンソースで開発が行われておりチケット管理にJiraを使用している。
https://issues.apache.org/jira/projects/HADOOP/summary
HDFS
HDFSとは巨大なデータを扱うとことに特化した分散ファイルシステム。データサイズとして想定しているのは1ファイルが数GB以上に及ぶようなファイルであり、そのような大きいファイルを高速に扱うことに特化している。HDFS自体にレプリケーションの対障害設定があり、基本的にはHDFSの役割を果たすノードにRAID構成は不要とされている。HDFSはバッチ処理に的するように作られておりデータの書き込みは一度だけで、それ以降は読み込み及び新しい書き込みが行われ、データの一部更新はできないようになっている。
Mapreduce
多数のサーバを利用して巨大なデータを分散処理するためのフレームワーク。Mapreduceではクラスター上の各ノードに以下の役割がある。
○NameNode
データがどこに配置されているかなどのメタデータを管理する。各ノードに対してハートビートで死活監視を行う。NameNode自体が死んだ時のためにSecondaryNameNodeを設定することもできる。
○JobTracker
MapReduceのマスターサーバで1つのジョブをタスクと呼ばれる複数の処理に分割し、各スレーブにタスクを割り振る
○DataNode
データを保存する
○TaskTracker
タスクを処理する
既存のMapreduceでは5000ノードからなる40000タスクを実行した時にJobTrackerがボトルネックとなることが確認されており、これは単一のJobTrackerに次の2つの異なる責任が課せられているためとされている。
○クラスターでの計算リソースの管理
○クラスター上で実行されるすべてのタスクの調整
この問題を解決するために単一のJobTrackerを使用するのではなく、クラスター・マネージャーを導入するようになった。クラスター・マネージャーの役割はクラスター内のライブ・ノードと使用可能なリソースを追跡して、タスクに割り当てることである。スレーブ側のTaskTracker側で短時間存続する JobTrackerが開始されるようになったが、これによりジョブのライフサイクルの調整は、クラスター内で使用可能なすべてのマシンに分散されさらに多くのタスクを分散処理できるようになった。この流れでYARNが登場した。
YARN
JobTrackerが担っていた2つの役割のを分割し、スレーブ側でタスクの調整が行えるよう疎結合になったことでhadoop自体の分散処理エンジン以外を使うことができるようになりSparkで処理を実行することができるようになっている。Mapreduceの時に使っていた用語は以下のように変わっている。
クラスター・マネージャー → ResourceManager
短時間存続する専用の JobTracker → ApplicationMaster
TaskTracker → NodeManager
ジョブ → 分散アプリケーション
YARNの登場により従来のMapreduceはMRv1、YARNがMRv2という扱いになった。Hadoopはversion2からYARNが使えるようになった。
Sparkについて
概要
scalaで書かれた並列分散処理のエンジン部分。HDFS上のデータにアクセスしてデータを処理することができる。Hadoopとの違いとして、Hadoopの分散処理エンジンでは計算結果をメモリ上には保存せずディスクに保存する作りになっている。それに比べてSparkでは計算結果をメモリ上に保存して再利用するという仕組みがあるためHadoopの分散処理エンジンと比べるとディスクI/Oの影響で数倍早いと言われている。このように仕組みが異なっている原因としてはHadoopとSparkの開発時期の違いが考えられ、Hadoopの開発が始まった頃はサーバ1台あたりのメモリを8GBくらいで想定していたのに対しSparkの開発時にはサーバ1台でメモリ100GB以上とか積んでいるのも珍しくないような時代背景の違いが考えられる。
用途
Sparkは以下のような用途で使われている。
○バッチ処理
単純にhadoopよりも早いためバッチ処理もsparkが使われることが多いきがする
○機械学習処理
機械学習のライブラリ(MLib)がありhadoopで使っていたMahoutより高速化できたという事例がある
○ストリーム処理
HDFS以外でもKafkaから送られてくるストリーミングデータを処理することができ、これによりバッチ処理、ストリーミング処理の両面でリアルタイムな機械学習でレコメンドするといったことが可能になっている
hadoopクラスタ管理
hadoopクラスタでは複数のサーバを扱わなければいけないので管理の面で気をつけなければいけないが、Clouderaが出しているマネジメントツールにより各ノードの管理が行いやすくなるらしい。clouderaのマネジメントツールを使わないにしてもディストリビューションで管理している対象ツールのバージョンを確認することでHadoopクラスタ構築時にどのバージョンを入れれば良いのかの参考になりそう(Hadoop, Spark, Hiveなど)
Hadoopクラスタ構築
実際にHadoopクラスタを構築してみたいと思います。
1.javaのインストール
hadoopはopenJdkではなくOracleのjdkを推奨している
2.プログラムのダウンロード
今回はHadoopとSpark,Hiveをインストールする
wget http://ftp.jaist.ac.jp/pub/apache/hadoop/common/hadoop-2.6.5/hadoop-2.6.5.tar.gz wget https://d3kbcqa49mib13.cloudfront.net/spark-1.6.0-bin-hadoop2.6.tgz wget https://archive.apache.org/dist/hive/hive-0.12.0/hive-0.12.0-bin.tar.gz
3.プログラムの展開及び配置
4.環境変数を追加
vi ~/.bashrc
以下を追加
# Hadoop export HADOOP_HOME=HADOOPの展開先 export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop export PATH=$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$PATH # spark export SPARK_HOME=Sparkの展開先 export PATH=$SPARK_HOME/bin:$PATH # hive export HIVE_HOME=Hiveの展開先 export PATH=$HIVE_HOME/bin:$PATH
source ~/.bashrc
HADOOP_CONF_DIRを環境変数に追加しているが、これはSparkで使用している。
5.設定ファイルの編集
HADOOP_CONF_DIRのディレクトリの以下の設定ファイルを編集します。
core-site.xml : マスターノードの設定 hdfs-site.xml : データ保存の設定、マスター側とスレーブで設定は異なる hive-site.xml : hive展開時のconfディレクトリにあったhive-default.xml.templateをリネーム mapred-site.xml : Mapreduceの設定 yarnを使う場合はここで指定する yarn-site.xml : リソースマネージャの設定
上記設定ファイルでサーバを指定する場合はIPアドレスではなくFQDNで指定しなければいけないので注意が必要
○core-site.xml
<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://ネームノードのFQDN:9000</value> </property> </configuration>
<configuration> <property> <name>dfs.replication</name> <value>3</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/data/1/dfs/nn</value> </property> </configuration>
<configuration> <property> <name>dfs.replication</name> <value>3</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/data/1/dfs/dn,/data/2/dfs/dn,/data/3/dfs/dn</value> </property> </configuration>
○mapred-site.xml
<configuration> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property> </configuration>
○yarn-site.xml
<configuration> <!-- Site specific YARN configuration properties --> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <property> <name>yarn.resourcemanager.hostname</name> <value>リソースマネージャのFQDN</value> </property> <property> <name>yarn.web-proxy.address</name> <value>WebAppProxyのFQDN:9046</value> </property> </configuration>
7.サービスの起動
設定が終わったらサービスを起動させます。NameNodeリソースマネージャが同一で、それ以外にDataノード兼TaskTrackerのスレーブが連なる場合は、以下のようになります。
- マスターノード
# $HADOOP_HOME/sbin/start-all.sh # yarn-daemon.sh start proxyserver # start-yarn.sh # mr-jobhistory-daemon.sh start historyserver
- スレーブノード
# $HADOOP_HOME/sbin/start-all.sh # start-yarn.sh
サービス起動後は各ノードでjpsを実行しサービスが立ち上がっているか確認します。それから以下URLにアクセスします。
http://ネームノードのFQDN:50070/dfshealth.html#tab-datanode
http://ノードマネージャのFQDN:8088/cluster/nodes
8.hdfsコマンドの確認 基本的なhdfsコマンドを実行しファイルのアップロードと参照が可能か確認します。
hdfs dfs -ls / hdfs dfs -mkdir /input echo "upload test" > test.txt hdfs dfs -put test.txt /input hdfs dfs -cat /input/test.txt
上記実行ご別のノードでもアップロードしたファイルが参照できるか確認してみます。
Hadoopクラスタ上でSparkのプロジェクトを動かしてみる
Sparkの簡単なプロジェクトを作成してHadoopクラスタ上で動かしてみたいと思います。
1.プロジェクト作成
まずsbt newかintelijの新規プロジェクト作成でプロジェクトを作成します。
2.build.sbtの設定
今回はspark-coreだけで十分のはずですが一応spark-sqlとかもlibraryDependenciesに追加しています。今回はjarを生成してsparkのコマンド経由で実行するのですが、jarを生成できるようにassemblyのプラグインを追加しています。またlibraryDependenciesでspark関連をprovidedで指定することも注意してください。
name := "StudySpark" version := "1.0" scalaVersion := "2.11.8" val sparkVersion = "2.1.0" // Intelijより直接実行する場合はspark関係はcompileにする、sbt assemblyでjarを出力する場合はprovidedにしておく libraryDependencies ++= Seq( "org.apache.spark" % "spark-core_2.11" % sparkVersion % "provided", "org.apache.spark" % "spark-sql_2.11" % sparkVersion % "provided", "org.apache.spark" % "spark-mllib_2.11" % sparkVersion % "provided", "org.apache.spark" % "spark-graphx_2.11" % sparkVersion % "provided", "org.apache.spark" % "spark-hive_2.11" % sparkVersion % "provided", "joda-time" % "joda-time" % "2.9.7" ) // assembly settings assemblyJarName in assembly := "studyspark.jar" assemblyMergeStrategy in assembly := { case PathList("javax", "servlet", xs @ _*) => MergeStrategy.first case PathList(ps @ _*) if ps.last endsWith ".html" => MergeStrategy.first case "application.conf" => MergeStrategy.concat case "unwanted.txt" => MergeStrategy.discard case x => val oldStrategy = (assemblyMergeStrategy in assembly).value oldStrategy(x) }
3.サンプルのプログラム作成
よく使われるワードカウントのプログラムを作ります。
package intoroduction.spark import org.apache.spark.{SparkConf, SparkContext} /** * 実行コマンド * spark-submit --master yarn \ * --class intoroduction.spark.WordCount \ * --name WordCount target/scala-2.11/studyspark.jar \ * /input/test.txt * */ object WordCount { def main(args: Array[String]) = { require(args.length >= 1, "ファイルを指定してください") val conf = new SparkConf().setAppName("WordCount").setMaster("local[*]") val sc = new SparkContext(conf) try { val filePath = args(0) val wordAndCountRDD = sc.textFile(filePath) .flatMap(_.split("[ ,.]")) .filter(_.matches("""\p{Alnum}+""")) .map((_, 1)).reduceByKey(_ + _) wordAndCountRDD.collect.foreach(println) } finally { sc.stop() } } }
4.jar生成
以下のコマンドでjarを生成します。
sbt assembly
5.プログラム実行
それから以下のコマンドでプログラムを実行します。
spark-submit --master yarn \ --class intoroduction.spark.rdd.WordCount \ --name WordCount target/scala-2.11/studyspark.jar \ /input/test.txt
うまくいけばコンソールに実行結果が出力されるはずです。 実行ステータスはノードマネージャのweb画面から見ることもできます。 http://ノードマネージャのFQDN:8088/cluster/nodes
デバック実行したい場合は事前に以下の環境変数を追加しておく必要があります。
export SPARK_SUBMIT_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=7777
上記環境変数追加後はport=7777でリモートデバックできます。
Angularでカスタムディレクティブへの双方向バインドに対して調べてみた
Angularでカスタムディレクティブへの双方向バインドに対して調べてみたときのメモ
コンポーネント内のメンバ変数の変更を監視する
例えばコンポーネント内に以下のメンバ変数があったとします。
@Input() textInput: String = "";
このメンバ変数は親のコンポーネントから直接値を変更されることがあるため、値が変更されたタイミングに実行したメソッド等ある場合は監視が必要になりそうですが、以下のように@Inputで監視対象の変数を指定することで変更された際に何をするか指定することができます。
@Input('textInput') set updateInternalVal(externalVal) { this.textInput = externalVal; this.onEditChange(); }
自作のカスタムディレクティブに対して双方向バインディングしてみる
inputタグなど元からhtmlに存在しているタグであればng-modelでバインドすることでinputへの入力がバインドしている変数に直接反映されるし、バインドしている変数自体を変更することでバインド先へのinputタグの表示が切り替わる双方向バインドが働いてますが、Angular2以降では自作のディレクティブに対しての双方向バインドが廃止されたようでちょっと工夫が必要なようでした。
以下自分が試した方法になります。
Outputを受け取る際に変数を変更する
例えば以下のコンポーネントがあったとします。 myText.html
<input [(ngModel)]="textInput" (ngModelChange)="onEditChange()" placeholder="{{ placeHolder }}"/> {{ textLength }}
myText.ts
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; @Component({ selector: 'my-text', templateUrl: './myText.html', styleUrls: [ './myText.css' ] }) export class MyTextComponent { @Input() textInput: String = ""; @Input() placeHolder: String = ""; textLength: Number = 0; @Output() textChange = new EventEmitter<String>(); // 親コンポーネントからの値の変更時に実行 @Input('textInput') set updateInternalVal(externalVal) { this.textInput = externalVal; this.onEditChange(); } onEditChange(): void { if(this.textInput !== void 0 && this.textInput.length !== void 0){ this.textLength = this.textInput.length; } this.textChange.emit(this.textInput); } }
このコンポーネントはメンバ変数textInputに親からバインドしているものがセットされているのですが、コンポーネント内で変数の値が変更されたことを親へ伝えるのには以下のようにカスタムのイベントを発行しています。
this.textChange.emit(this.textInput);
その場合、呼び出し元の親コンポーネントは以下のようにtextChangeのイベントを受け取った際にバインド先の変数を変更することで双方向バインドの動きになります。
<my-text [(textInput)]="hero.name" (textChange)="hero.name = $event" placeHolder="name"></my-text><br />
Outputのカスタムイベント名を"メンバ変数 + Change"にすることで自動で双方向バインドする
さっきの方法だと親コンポーネント側で双方向バインドするか選べましたが、こっちの方は子コンポーネントの方で直接双方向バインドするように指定できます。 まず子コンポーネント側でカスタムイベントの名称を"メンバ変数 + Change"に指定し、値に変更があったらemitするようにします。 myText.ts
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; @Component({ selector: 'my-text', templateUrl: './myText.html', styleUrls: [ './myText.css' ] }) export class MyTextComponent { @Input() textInput: String = ""; @Input() placeHolder: String = ""; textLength: Number = 0; @Output() textInputChange = new EventEmitter<String>(); // 親コンポーネントからの値の変更時に実行 @Input('textInput') set updateInternalVal(externalVal) { this.textInput = externalVal; this.onEditChange(); } onEditChange(): void { if(this.textInput !== void 0 && this.textInput.length !== void 0){ this.textLength = this.textInput.length; } this.textInputChange.emit(this.textInput); } }
この場合は、イベントが発行された時に何をするとか特に指定しなくても双方向バインドが実現されます。以下呼び出し元のサンプルになります。
<my-text [(textInput)]="hero.name" placeHolder="name"></my-text><br />
深層学習(青本) 1章. はじめに
深層学習の1章読書まとめ
研究の歴史
多層ニューラルネットへの期待と失望
ニューラルネットワークはこれまでに2度のブームが訪れていた
1回目のブーム
1960年代〜1970年代
パーセプトロン(1層ニューラルネット)
2回目のブーム
80年代半ばから90年代前半前半
誤差逆伝搬の誕生(多層ニューラルネット)
パーセプトロンの欠点
ノイズに弱い
収束が遅く、学習効率が悪い(誤差逆伝搬でいう損失関数の微分を効率的に使ったパラメータ更新が行えない)
n次元の入力に対して重み付けを行った結果がある値より大きいかどうかで2つのクラスに分類する
y=WtX (Wt:学習済みの重み、 X:入力、 y:2つのクラスを分類する境界)
線形分離可能な問題にしかできない(多層化できれば解決できる)
多層化できない(中間層を出力することができない)
誤差逆伝搬(多層ニューラルネット)の欠点
入力にノイズがあったら過学習は発生する
2層程度であれば期待通りだが、それより増やしていくと勾配が急速にに小さくなったり大きくなったりする勾配消失問題が発生する
※ニューラルネットの層を増やす動きがあるが、これは中間層を用意することで柔軟なネットワークを構築でき学習精度を高めることができるからである、誤差逆伝搬により多層化は行えるが層が増えると勾配が消失しあまり精度が上がらなくなるという問題がある
※この頃はニューラルネットの層数やユニット数(中間層の主力)の設定に対しての理論がなかった
畳み込みニューラルネットの誕生
1980年代後半画像を対象としていた畳み込みニューラルネットについてはこの時点で5層からなる多層ネットワークの学習に成功していた
※前の層の畳み込みの出力を粗くリサンプリングするようなイメージとなっており、これにより画像の多少のずれによる見え方の違いを吸収することが可能となり勾配消失問題が発生しなかった
1980年代半ばからのブームの多層ニューラルネットワークでは各層の出力全てを次の層に入力していた(全結合)
多層ニューラルネットワーク自体の研究低下とともに畳み込みニューラルネットの関心も小さくなっていった
多層ネットワークの事前学習
1990年代〜2000年代前半(ニューラルネットの関心が低い時期)
ディープビリーフネットワーク(DBN)の研究
一つの層を貪欲アルゴリズムで訓練できることでブレークスルーとなった
制約ボルツマンマシン(RBM)の誕生
その後、自己符号化の登場
1990年代〜2000年代前半では事前学習によりノイズ除去などで良い入力を得られるようにする研究が行われた
特徴量の学習
画像や音声などの自然界のデータなど同じものでもちょっとの違いでデータに差が出るものをネットワークの多層構造に取り込むために、どのように特徴量を抽出するか
例えば猫の画像でも明るさが違ったり、向きが違ったりするだけでデータとして差が出る
音声の場合は白色ノイズなど、扱うデータの種類によりノイズの種類も様々
正規化することでそれらの違いを吸収した特徴量の情報が欲しい
画像や音声については事前学習ではなく事前処理による特徴量抽出で良い入力を得られるようにしていた
深層学習の隆盛
音声認識、画像認識のベンチマークで多層ニューラルネットワークの有効性が認められるようになり深層学習の有効性が広く認知されるようになった
一重に深層学習といっても扱う対象によって学習方法が異なる
・音声認識の場合は層間ユニット全結合で事前学習が一般的に行われる
・画像認識では畳込みニューラルネットが主流で事前学習は不要
・自然言語処理、音声認識の特定のタスクでは再帰型ニューラルネット
それぞれ学習方法が違う多層ニューラルネットワークで性能を把握できるようになった
→共通の理由は計算機の計算能力の飛躍的な功により扱えるデータが増えた
GPU Technology ConferenceとかみるとGPUの発達が深層学習に与える影響が大きそうに感じる
計算機の処理能力向上によりニューラルネットワークの多層化をしたら思わぬ結果が出た
AnularJSのチュートリアルをやってみた
Angularのチュートリアルを通して使い方を覚えたいと思います。
今回学習用で作成したプロジェクトは以下からとってこれるようにしています。
GitHub - teruuuuuu/angular_handson
- 開発環境構築
- 開発
- 触ってみた感想
開発環境構築
angular-cliを使えるようにする
npm install -g @angular/cli
2.プロジェクトを作ってみる
ng new my-app
3.アプリを起動する
cd my-app
ng serve –open
“ng serve"で開発用のサーバを起動しファイルに対して変更があればすぐに修正を反映する動きをします。–openのオプションをつけることでコマンド実行時にブラウザを自動で開きます。
Atomで開発
Atomで開発する場合はプラグインを探して入れておけば良さそうです。 atom-typescriptをとりあえず入れておいた
開発
angular-cliでのアプリ開発について
.angular-cli.json
開発環境の設定は.angular-cli.jsonで行なっており、最初に読み込みhtmlやスクリプト、ルートディレクトリやビルド時の出力先もここで設定している。アプリ全体でスタイルやスクリプトを適用したいという場合はここのscriptとstylesや、デフォルトのstyles.cssを修正する。アプリのルートディレクトリはsrcに設定してある。
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { "name": "my-app" }, "apps": [ { "root": "src", "outDir": "dist", "assets": [ "assets", "favicon.ico" ], "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", "test": "test.ts", "tsconfig": "tsconfig.app.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ], "e2e": { "protractor": { "config": "./protractor.conf.js" } }, "lint": [ { "project": "src/tsconfig.app.json" }, { "project": "src/tsconfig.spec.json" }, { "project": "e2e/tsconfig.e2e.json" } ], "test": { "karma": { "config": "./karma.conf.js" } }, "defaults": { "styleExt": "css", "component": {} } }
index.html
angular-cliで作成したプロジェクトのindex.htmlは以下のようになっている。app-rootという独自のタグを読み込んでいます。コンポーネントが初期化されるまではタグの中身の"Loading…“が表示されます。
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>AngularHandson</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> </head> <body> <app-root>Loading...</app-root> </body> </html>
main.ts
main.tsではアプリに必要なモジュールを読み込んでいます。環境変数などはJsonを読み込ませるかプロパティに直接定義するなどしてenvironmentで読み込めるようにしておくと良さそうです。リリース時はenableProdModeが有効になるようですが、angularの開発モードが無効になるなどの違いがあるようです。
import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './main.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic().bootstrapModule(AppModule);
main.module.ts
main.tsで読み込んでいるモジュール。NgModuleでページの単位となるコンポーネント一覧を読み込んでいるのが確認できる。bootstrapはエントリポイントを指定していて、providersにはデータ共有に使うサービスとかを指定して、importsでは外部のモジュールを読み込んだりしていて、declarationsではディレクティブとパイプを読み込ませるらしい。ディレクティブとは先ほどのapp-rootなど独自に定義したタグのことを言っており、パイプについてはangularのhtml内で条件指定でフィルターかけるのに使う。BrowserModuleはブラウザの情報を取ってくるので必要になるっぽい。
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
index.component.ts
Componentアノテーションのselectorではディレクティブと使用するタグ名、templeteUrlでテンプレートhtml、styleUrlsでcssを指定してからclassないでスクリプトを記述している。titleはクラス変数でapp.component.htmlの表示で使用している。TypeScriptのクラス変数ではconstやletを付与しようとしたらコンパイラに怒られたので普遍にする場合はstatic readonlyとかつけたら良さそうと思ったけどそうしたらbindできなくなる動きをしていた。
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './index.component.html', styleUrls: ['./index.component.css'] }) export class AppComponent {}
index.component.html
index.component.tsのhtml表示用のテンプレートは最初は以下のようになっています。
<!--The whole content below can be removed with the new code.--> <div style="text-align:center"> <h1> </h1> </div>
htmlとコンポーネントで2wayバインディングさせてみる
まずmain.module.tsで2wayバインディングに必要となるFormModuleを読み込ませます main.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; //テンプレートでバインディングしたり、validationするのに必要 import { AppComponent } from './index.component'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
それからindex.component.tsとindex.component.htmlでコンポーネント内のクラス変数をbindさせてみます。 index.component.ts
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './index.component.html', styleUrls: ['./index.component.css'] }) export class AppComponent { textInput = "input test"; }
index.component.html
<!--The whole content below can be removed with the new code.--> <div style="text-align:center"> <h1> <input [(ngModel)]="textInput"><br /> {{ textInput }} </h1> </div>
これで実際に動かしてみると画面にコンポーネントのクラス変数が表示され、またinputタグへ入力を行いbindしている変数を変更すると値自体が変更され表示にも反映されるのが確認できます。
ルータとサービスを使ってみる
angularのルータとサービスを利用してみたいと思います。angularのルータにより表示するコンポーネントを切り替えたりURLパラメータを受け取った処理ができるようになります。サービスはAPIを投げてデータを取得したりとか、コンポーネント間で共有するデータとかデータに関する操作とかを記述します。 まずルータを足してみます。 app/router/app.router.ts
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component'; // コンポーネントとURLを関連づける const routes: Routes = [ { path: '', redirectTo: '/detail/1', pathMatch: 'full' }, // URLパラメータをコンポーネントに渡すようにしている { path: 'detail/:id', component: HeroDetailComponent }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
app/service/hero.service.ts
import { Injectable } from '@angular/core'; import { Hero } from 'app/model/Hero'; import { HEROES } from 'app/mock/heros.mock'; @Injectable() export class HeroService { getHeroes(): Promise<Hero[]> { console.log("hero service getHeros"); console.info(HEROES); // 2wayバインドによりmockオブジェクト自体が変更されていることが確認できる return Promise.resolve(HEROES); } getHeroesSlowly(): Promise<Hero[]> { return new Promise(resolve => { // Simulate server latency with 2 second delay setTimeout(() => resolve(this.getHeroes()), 2000); }); } // id指定でデータ取得 getHeroById(id: number): Promise<Hero> { return this.getHeroes() .then(heroes => heroes.find(hero => hero.id === id)); } }
サービスはインポートしたHEROESオブジェクトをPromiseにより非同期でレスポンスとして返す処理を行います。今回使用しているモック用のオブジェクトは以下のようになっています。 app/mock/heros.mock.ts
import { Hero } from 'app/model/Hero'; export const HEROES: Hero[] = [ { id: 1, name: 'Mr. Nick' }, { id: 11, name: 'Mr. Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, { id: 15, name: 'Magneta' }, { id: 16, name: 'RubberMan' }, { id: 17, name: 'Dynama' }, { id: 18, name: 'Dr IQ' }, { id: 19, name: 'Magma' }, { id: 20, name: 'Tornado' } ];
それと、Heroの型は以下のようになっています。 app/model/Hero.ts
export class Hero { id: number; name: string; }
次にルータとサービスをモジュールとして使えるようにコンポーネントに読み込ませます。 main.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; //テンプレートでバインディングしたり、validationするのに必要 import { AppComponent } from 'index.component'; import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component'; import { HeroService } from 'app/service/hero.service'; import { AppRoutingModule } from 'app/router/app.router'; @NgModule({ imports: [ AppRoutingModule, // 注意 ルータはdeclationではなくimportsにたす BrowserModule, FormsModule ], declarations: [ AppComponent, HeroDetailComponent ], providers: [ HeroService // サービスはprovidersに追加する ], bootstrap: [AppComponent] }) export class AppModule { }
次にルータとサービスで使用するHeroDetailComponentを追加してみたいと思います。
app/component/heroDetail/hero.detail.component.ts
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { Hero } from 'app/model/Hero'; import { HeroService } from 'app/service/hero.service'; @Component({ selector: 'hero-detail', //ディレクティブのタグ名 templateUrl: './hero.detail.component.html' //htmlテンプレートの読み込み }) export class HeroDetailComponent implements OnInit { // テンプレートhtmlにbindして使用するクラス変数 title = 'HeroDetail'; hero: Hero = new Hero(); // コンポーネントを使用する側で用途を決めれるようにする isSearchMode: Boolean = true; constructor( private heroService: HeroService, // urlパラメータを取得するのに必要 private route: ActivatedRoute) { } ngOnInit(): void { if(this.isSearchMode){ // ルータからパラメータ取得 this.route.params.forEach((params: Params) => { console.log("hero detail component ngOnInit"); console.info(params); if (params['id'] !== undefined) { const id = +params['id']; this.heroService.getHeroById(id) .then(hero => this.hero = hero); } }); } } }
app/component/heroDetail/hero.detail.component.html
<!-- heroが見つかった時のみこの部分を表示する--> <div *ngIf="hero"> <h1>{{title}}</h1> <h2>{{hero.name}} details!</h2> <div><label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <!-- 注意 ngMoelを使う場合はNgModuleでFormsModuleをインポートしないといけない--> <input [(ngModel)]="hero.name" placeholder="name"> </div> </div> <!-- heroが見つからなかった場合の処理 --> <div *ngIf="!hero"> hero not found. </div>
app/component/heroDetail/hero.detail.component.css
h1 { color: #369; font-family: Arial, Helvetica, sans-serif; font-size: 250%; }
それから、ルータのディレクティブをhtmlテンプレートに追加して使用できるようにします。 index.component.html
<!--The whole content below can be removed with the new code.--> <div style="text-align:center"> <h1> <input [(ngModel)]="textInput"><br /> {{ textInput }}<br /> </div> <!-- ルータ配置用のテンプレート --> <router-outlet></router-outlet>
これで動かしてみると'http://localhost:4200'のアクセスは'http://localhost:4200/detail/1'にリダイレクトされid=1のユーザが表示されるはずです。idの部分のパラメータを変更することで表示するユーザが切り替わることが確認できます。
別コンポーネントでも同一のサービスを使ってみる
別コンポーネントで同一のサービスを利用し2way-bindingによりサービス館でデータが共有されていることを確認します。 まず今回使用するサービスに以下のメソッドを追加します。先に追加している非同期の処理でも大丈夫ですが今回は既にサービスコンポーネントで保有されているデータを返す処理を追加したく非同期である必要はなさそうなのでそれ用のメソッドを追加しています。
// データ共有をするだけの用途とかでPromiseを使わないこともできる getSyncHero(id: number): Hero { return HEROES.find(hero => hero.id === id); }
それから、以下のように既存のindex.component.tsでサービスを利用するように変更します。 index.component.ts
import { Component, OnInit } from '@angular/core'; import { Hero } from 'app/model/Hero'; import { HeroService } from 'app/service/hero.service'; @Component({ selector: 'app-root', templateUrl: './index.component.html', styleUrls: ['./index.component.css'] }) export class AppComponent implements OnInit { textInput = "input test"; hero: Hero = new Hero(); constructor( private heroService: HeroService) { } ngOnInit(): void { this.hero = this.heroService.getSyncHero(1); } }
index.component.html
<!--The whole content below can be removed with the new code.--> <div style="text-align:center"> <h1> <input [(ngModel)]="textInput"><br /> {{ textInput }}<br /> <input [(ngModel)]="hero.name" placeholder="name"><br /> {{ hero.name }} </h1> </div> <!-- ルータ配置用のテンプレート --> <router-outlet></router-outlet>
これでindex.componentにもHeroの情報が表示され、またそれぞれのinputに対して入力すると即時で反映されることが確認できます。
htmlテンプレートでループ処理をする
次にhtmlテンプレート内でループを回して描画を行ってみたいと思います。リストの情報を取得する以下のコンポーネントを追加します。
app/component/heroList/hero.list.component.ts
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Hero } from 'app/model/Hero'; import { HeroService } from 'app/service/hero.service'; @Component({ selector: 'hero-list', templateUrl: './hero.list.component.html', styleUrls: ['./hero.list.component.css'] }) export class HeroListComponent implements OnInit { heroes: Hero[] = []; title = 'HeroesList'; selectedHero: Hero; // サービスはconstructorに足しておく constructor( private router: Router, private heroService: HeroService) { } ngOnInit(): void { // 再描画のたびに呼ばれるので、ここでメンバ変数を初期化 console.log("HeroListComponent ngOnInit") this.heroService.getHeroes() .then(heroes => this.heroes = heroes); /* this.heroService.getHeroesSlowly() .then(heroes => this.heroes = heroes); */ } onSelect(hero: Hero): void { this.selectedHero = hero; } }
それから、htmlテンプレートを作成します。
<h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero"> <span class="hero-element"> <span class="badge">{{hero.id}}</span> {{hero.name}}</span> </li> </ul>
上記の<li *ngFor=“let hero of heroes” ~の部分がコンポーネント内のメンバ変数であるheroesをループさせて描画処理を
行っています。hero === selectedHeroの条件が一致している場合はタグのクラスに"selected"を追加します。
それとcssも作成しておきます。
.selected { background-color: #CFD8DC !important; background-color: rgb(0,120,215) !important; color: white; } .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { cursor: pointer; position: relative; left: 0; background-color: #EEE; margin: .5em; padding: .5em; height: 1.6em; border-radius: 4px; } .heroes li:hover { color: #607D8B; color: rgb(0,120,215); background-color: #DDD; left: .1em; } .heroes li.selected:hover { /*background-color: #BBD8DC !important;*/ color: white; } .heroes .text { position: relative; top: -3px; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #607D8B; background-color: rgb(0,120,215); line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0 0 4px; } button { font-family: Arial; background-color: #eee; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; cursor: hand; } button:hover { background-color: #cfd8dc; } .error {color:red;} button.delete-button{ float:right; background-color: gray !important; background-color: rgb(216,59,1) !important; color:white; }
あとは、main.moduleに今回のモジュールを追加して、ルータでURLとコンポーネントを関連づけることで表示が行えます。 main.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; //テンプレートでバインディングしたり、validationするのに必要 import { AppComponent } from 'index.component'; import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component'; import { HeroListComponent } from 'app/component/heroList/hero.list.component'; import { HeroService } from 'app/service/hero.service'; import { AppRoutingModule } from 'app/router/app.router'; @NgModule({ imports: [ AppRoutingModule, // 注意 ルータはdeclationではなくimportsにたす BrowserModule, FormsModule ], declarations: [ AppComponent, HeroDetailComponent, HeroListComponent ], providers: [ HeroService ], bootstrap: [AppComponent] }) export class AppModule { }
app/router/app.router.ts
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component'; import { HeroListComponent } from 'app/component/heroList/hero.list.component'; // コンポーネントとURLを関連づける const routes: Routes = [ { path: '', redirectTo: '/list', pathMatch: 'full' }, { path: 'detail/:id', component: HeroDetailComponent }, { path: 'list', component: HeroListComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
子コンポーネントにデータを渡してみる
サービスを使ってコンポーネント間でデータが共有できるのは確認できましたので、次はサービスを使わずに直接コンポーネントに対してデータが渡せる確認してみたいと思います。
まず、app/component/heroList/hero.list.component.htmlに以下を追加します。
<!-- コンポーネントのメンバ変数を[]で囲ったものに対して選択したheroを渡す --> <hero-detail [hero]="selectedHero" [isSearchMode]="false"></hero-detail>
それからapp/component/heroDetail/hero.detail.component.tsのメンバ変数に@Input()を付与することで親コンポーネントからデータを受け取ることができるようになります。 app/component/heroDetail/hero.detail.component.ts
import { Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { Hero } from 'app/model/Hero'; import { HeroService } from 'app/service/hero.service'; @Component({ selector: 'hero-detail', //ディレクティブのタグ名 templateUrl: './hero.detail.component.html' //htmlテンプレートの読み込み }) export class HeroDetailComponent implements OnInit { // テンプレートhtmlにbindして使用するクラス変数 title = 'HeroDetail'; @Input() hero: Hero = new Hero(); // コンポーネントを使用する側で用途を決めれるようにする @Input() isSearchMode: Boolean = true; constructor( private heroService: HeroService, // urlパラメータを取得するのに必要 private route: ActivatedRoute) { } ngOnInit(): void { if(this.isSearchMode){ // ルータからパラメータ取得 this.route.params.forEach((params: Params) => { console.log("hero detail component ngOnInit"); console.info(params); if (params['id'] !== undefined) { const id = +params['id']; this.heroService.getHeroById(id) .then(hero => this.hero = hero); } }); } } }
コンポーネント間で画面遷移してみる
コンポーネント間で遷移できるようにするためまずapp/component/heroList/hero.list.component.tsに以下のメソッドを追加します。
gotoDetail(): void { this.router.navigate(['/detail', this.selectedHero.id]); }
それから、app/component/heroList/hero.list.component.htmlからgotoDetailを呼び出せるようにするため、以下のように修正します。
<h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero"> <span class="hero-element"> <span class="badge">{{hero.id}}</span> {{hero.name}}</span> </li> </ul> <!-- コンポーネントのメンバ変数を[]で囲ったものに対して選択したheroを渡す --> <!-- <hero-detail [hero]="selectedHero" [isSearchMode]="false"></hero-detail> --> <div *ngIf="selectedHero"> <h2> {{selectedHero.name | uppercase}} is my hero </h2> <button (click)="gotoDetail()">View Details</button> </div>
次にapp/compnent/heroDetail/hero.detail.compnentでは遷移元に戻れるように以下のメソッドを追加します。
goBack(savedHero: Hero = null): void { window.history.back(); }
それからapp/compnent/heroDetail/hero.detail.compnent.htmlから呼び出せるように以下のように修正します。
<!-- heroが見つかった時のみこの部分を表示する--> <div *ngIf="hero"> <h1>{{title}}</h1> <h2>{{hero.name}} details!</h2> <div><label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <!-- 注意 ngMoelを使う場合はNgModuleでFormsModuleをインポートしないといけない--> <input [(ngModel)]="hero.name" placeholder="name"><br /> <button (click)="goBack()">Back</button> </div> </div> <!-- heroが見つからなかった場合の処理 --> <div *ngIf="!hero"> hero not found. </div>
これでコンポーネント間での画面繊維が確認できたかと思います。
もう一つコンポーネントを追加してみる
次にダッシュボードコンポーネントを追加して、こちらからもhero.detail.compnentに遷移できるようにしたいと思います。 以下のapp/component/dashboard/dashboard.component.tsを作成します。
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Hero } from 'app/model/Hero'; import { HeroService } from 'app/service/hero.service'; @Component({ selector: 'my-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.css'] }) export class DashboardComponent implements OnInit { heroes: Hero[] = []; constructor( private router: Router, private heroService: HeroService) { } ngOnInit(): void { this.heroService.getHeroes() .then(heroes => this.heroes = heroes.slice(1, 6)); } gotoDetail(hero: Hero): void { const link = ['/detail', hero.id]; this.router.navigate(link); } }
それからhtmlテンプレートを作成します。
<div class="grid grid-pad"> <h3>Top Heroes</h3> <div *ngFor="let hero of heroes" (click)="gotoDetail(hero)" class="col-1-4"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </div> </div>
あとはmain.module.tsでDashboardComponentを読み込むようにし、ルータに追加しておくと画面が表示されるようになります。
httpリクエストを投げれるようにしてみる
次にhttpリクエストを投げれるようにしてみます。リクエストを受けるWEBサーバを準備するのは面倒なのでangularのモックを利用します。そのためにはmain.module.tsで以下のモジュールをインポートするようにします。
import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; // npm install --save angular-in-memory-web-api
“angular-in-memory-web-api"はangular本体に組み込まれていないので以下のコマンドでインストールしておきます。
npm install –save angular-in-memory-web-api
それから、レスポンスとして返すデータを定義するapp/service/in-memory-data.service.tsを作成します。
// angular-in-memory-web-apiで使うモックのapiの初期データ export class InMemoryDataService { createDb() { const heroes = [ { id: 1, name: 'one' }, { id: 11, name: 'Mr. Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, { id: 15, name: 'Magneta' }, { id: 16, name: 'RubberMan' }, { id: 17, name: 'Dynama' }, { id: 18, name: 'Dr IQ' }, { id: 19, name: 'Magma' }, { id: 20, name: 'Tornado' } ]; return { heroes }; } }
ここまで済んだらmain.module.tsを以下のように修正しWebAPIのモックを使用できるようにします。
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; //テンプレートでバインディングしたり、validationするのに必要 import { HttpModule } from '@angular/http'; // httpサービスを利用するのに必要 // 今回はWebAPIのモックを使用する import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; // npm install --save angular-in-memory-web-api import { InMemoryDataService } from 'app/service/in-memory-data.service'; import { AppComponent } from 'index.component'; import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component'; import { HeroListComponent } from 'app/component/heroList/hero.list.component'; import { DashboardComponent } from 'app/component/dashboard/dashboard.component'; import { HeroService } from 'app/service/hero.service'; import { AppRoutingModule } from 'app/router/app.router'; @NgModule({ imports: [ AppRoutingModule, // 注意 ルータはdeclationではなくimportsにたす BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(InMemoryDataService) ], declarations: [ AppComponent, HeroDetailComponent, HeroListComponent, DashboardComponent ], providers: [ HeroService ], bootstrap: [AppComponent] }) export class AppModule { }
次にapp/service/hero.service.tsを修正しWebAPI経由でデータを取得するようにします。まず"angular/http"モジュールとrxjsのtoPromiseをインポートします。 angular/httpのレスポンスはrxjsのtoPromiseで非同期で扱うのでrxjsのインポートも必要になります。
import { Headers, Http, Response } from '@angular/http'; import 'rxjs/add/operator/toPromise';
それからコンポーネント内のメッソッドを以下のように修正しWebAPI経由でデータを取得するように変更します。
private heroesUrl = 'api/heroes'; // URL to web api private headers = new Headers({ 'Content-Type': 'application/json' }); constructor(private http: Http) { } getHeroes(): Promise<Hero[]> { console.log("hero service getHeros"); return this.http.get(this.heroesUrl) .toPromise() // jsonのレスポンスを受け取ってHero型の配列に変換する .then(response => response.json().data as Hero[]) .catch(this.handleError); } getHeroById(id: number): Promise<Hero> { const url = `${this.heroesUrl}/${id}`; return this.http.get(url) .toPromise() .then(response => response.json().data as Hero) .catch(this.handleError); } // httpリクエスト失敗時の処理 private handleError(error: any): Promise<any> { console.error('An error occurred', error); return Promise.reject(error.message || error); }
今回はrxjsを使用していますがレスポンスのjsonをHero型の配列に変換するだけなので
.toPromise() .then(response => response.json().data as Hero)
のようになっています。
動かしてみると一覧表示をする際に毎回データを撮り直しているため、heroの名前を変更して一覧に戻ると変更が反映されないというのが確認できるかと思います。これまではサービスをコンポーネント間でのデータの共有として使っていたのですが、今回の修正でサービスをWebサーバに対してサービスを投げる用途で使うようにしたのでその違いはわかるようにしておきたいです。例えば共通のAPIで取得した結果を複数のコンポーネントで使うという必要があるのでしたら、サービスコンポーネント内にデータ保有用の変数を用意しておきWebAPIを呼び出した後はその変数を変更するようにする必要があるかと思います。
追加、更新、削除のリクエストを投げれるようにしてみる
サービス側に追加、更新、削除のリクエストを投げるメソッドを追加します。
// angular-in-memory-web-apiのcreateApi呼び出し create(name: string): Promise<Hero> { return this.http .post(this.heroesUrl, JSON.stringify({ name: name }), { headers: this.headers }) .toPromise() .then(res => res.json().data as Hero) .catch(this.handleError); } // angular-in-memory-web-apiのupdateApi呼び出し update(hero: Hero): Promise<Hero> { const url = `${this.heroesUrl}/${hero.id}`; return this.http .put(url, JSON.stringify(hero), { headers: this.headers }) .toPromise() .then(() => hero) .catch(this.handleError); } delete(id: number): Promise<void> { const url = `${this.heroesUrl}/${id}`; return this.http.delete(url, { headers: this.headers }) .toPromise() .then(() => null) .catch(this.handleError); }
httpモジュールのpost、put、deleteを使い分けていますが違いはここのstackoverflowを確認するのが良さそうです。
あとは各コンポーネントからサービスを利用するようにしたら、heroの名前変更が一覧に反映されたり、登録、削除が確認できるかと思います。
検索を行ってみる
サービスに検索用のリクエストを投げるメソッドを追加します。
search(term: string): Observable<Hero[]> { return this.http .get(`app/heroes/?name=${term}`) .map(response => response.json().data as Hero[]); }
レスポンスをそのままHero型の配列にセットするだけなので今までと同様にtoPromise()~で大丈夫かと思ったのですが、getリクエストの場合はtoPromiseが使えないようです。 この辺りはrxjsとhttpモジュール周りの学習が必要になりそうです。 それから以下のapp/component/heroSearch/hero.search.componentモジュールを追加してdashboardコンポーネントに配置すると検索コンポーネントが使えるようになります。 app/component/search/hero.search.component.ts
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; // Observable class extensions import 'rxjs/add/observable/of'; // Observable operators import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; import 'rxjs/add/operator/switchMap'; import { HeroService } from 'app/service/hero.service'; import { Hero } from 'app/model/Hero'; @Component({ selector: 'hero-search', templateUrl: './hero.search.component.html', styleUrls: [ './hero.search.component.css' ], providers: [HeroService] }) export class HeroSearchComponent implements OnInit { heroes: Observable<Hero[]>; private searchTerms = new Subject<string>(); constructor( private heroService: HeroService, private router: Router) {} // Push a search term into the observable stream. search(term: string): void { this.searchTerms.next(term); } ngOnInit(): void { this.heroes = this.searchTerms .debounceTime(300) // wait 300ms after each keystroke before considering the term .distinctUntilChanged() // ignore if next search term is same as previous .switchMap(term => term // switch to new observable each time the term changes // return the http search observable ? this.heroService.search(term) // or the observable of empty heroes if there was no search term : Observable.of<Hero[]>([])) .catch(error => { // TODO: add real error handling console.log(error); return Observable.of<Hero[]>([]); }); } gotoDetail(hero: Hero): void { let link = ['/detail', hero.id]; this.router.navigate(link); } }
app/component/heroSearch/hero.search.comonent.html
<div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <div> <div *ngFor="let hero of heroes | async" (click)="gotoDetail(hero)" class="search-result" > {{hero.name}} </div> </div> </div>
app/component/heroSearch/hero.search.component.css
.search-result{ border-bottom: 1px solid gray; border-left: 1px solid gray; border-right: 1px solid gray; width:195px; height: 16px; padding: 5px; background-color: white; cursor: pointer; } .search-result:hover { color: #eee; background-color: #607D8B; } #search-box{ width: 200px; height: 20px; }
検索用のコンポーネントでは入力に変更があったらrxjsでサービスを呼び出すようにしています。angularのチュートリアルでrxjsが使われているので、angularをやるならrxjsを覚えていた方が良さそうに思いました。 あとは今までと同様main.module.tsでモジュールを読み込むようにしダッシュボードコンポーネントに表示するようにして動作は確認できるかと思います。
触ってみた感想
Angularの1は触ったことがあったのですが、それに比べてだいぶ分かりやすくて扱いやすくなったと思います。特にルータ周りはReactと比べて優位に立ってそうな気がしました。
JavaScriptフレームワーク調査
JavaScriptのモダンフレームワークでどれを選べば良いのか調査した内容になります。
AngularJS
googleによっって開発されているMVVMフレームワーク 現在Angular4まで出ている。出た当初は仮想DOMを使っているReactなどに比べると遅いと言われていたけど、現在はReactと比べて特に遅いというふうではないらしい。
テンプレート
HTMLテンプレートを作成しデータとtwo-wayバインディングさせる。ReactやVueなどの仮想DOMを使うフレームワークの場合はone-wayでデータをバインディングさせるが、Angularの場合は仮想DOMを使っていないためかtwo-wayバインディングで表示用のデータを手軽に操作することができる。 JavaScript内部にhtmlを記述してデータのバインドやテンプレートの読み込みをするなどAngularフレームワーク独特の学習は必要になりそう。
DI
作成したモジュールをサービスとして別のモジュールで利用する(DI)ことができる。コンポーネント間でのデータのやり取りでもサービスを使うらしい。
テスト
Angularのテンプレート構文はJSXのように静的チェックが行えないためテストツールが必須になっているとのことらしい。その分公式の方がどのテストフレームワークを使えば良いか説明していて、それに合わせて作っていたら問題なさそう。
所感
フルスタックフレームワークな分覚えることは多いけど、公式のサポートが充実してそうなので特に困ると言ったことはなさそうなきがする。他のReactやVueなどのフレームワークに比べるとHttpリクエストなどフレームワーク側でのサポートが広い範囲で行われるので安心して使うことはできそう。業務でSPAを扱うと言ったことがあればAngularが良さそうなきがする。
React
React自体はMVVMのViewの部分だけを扱うフレームワークでシンプルと言われているけど、Facebookが提唱するFluxやReduxのアーキテクチャに合わせて実装しようとするのであればその分書くコードは増えて複雑になる。React自体がシンプルな分どう使うかは開発者自身に委ねられている部分が多い気がする。学習コストについてReact自信を覚えるのは大変ではないけどどう設計するかで時間がかかるかもしれない。
テンプレート
テンプレートにはJSXを使用している。JavaScriptの中に直接HTMLを吐きだす関数が書かれているため、デザイナーでない人が見るのは大変だと思われる。JSX自体はReactを使っている開発者自体にも嫌われているので、今後の改善が望まれる。
テスト
公式ではJesとかenzymeが進められていて、Jsetの方が簡単に扱えそうな感じがした。 enzymeのを使用する場合は別でモック用のモジュールを入れている記事がいくつか見受けられる。
Redux
Reduxはだいたいこんな感じだと思う。
アプリケーション全体で管理するstateを変更する場合はマウスクリックなどのイベント後に、状態変更のためのアクションを実行する。
アクションによりデータが作成されたら変更を反映するため、ディスパッチといってstateを管理するstoreにデータを送る。
reduxではここでstateの変更をすぐに変更するのではなくmidlewareの処理を実行する。
midlewareはログの出力やサーバへのリクエスト投げたりするのに使われている気がする。
midlewareが増えると全体的に遅くなるという問題はあるようです。
midlewareの一連の処理が終わったらreducerによりアプリケーションの状態を表すstateが変更され、 そして仮想DOMが変更され描画に反映さる。
所感
Reduxの実装方法であればデータを厳密に扱うことができるので、例えば一つのデータに対していろんな見せ方があったり、いろんなところから変更したりという複雑さがある場合はReactを使った方がよさそうな気がした。
Vue
公式サイトよりVueの概要は以下のようになっています。
Vue.js (発音は / v j u ː /、view と同様) はインタラクティブな Web インタフェースを構築するためのライブラリです。Vue.js のゴールは、 できる限りシンプルな API でリアクティブデータバインディング と 構成可能な View コンポーネントを提供することです。 Vue.js 自体は本格的なフレームワークではありません、Vue.js は View レイヤーだけに焦点を当てています。したがって、Vue.js のいいところだけをピックアップしたり、Vue.js を他のライブラリや既存のプロジェクトに統合することはとても簡単です。一方、Vue.js を適切なツールとサポートするライブラリによる組み合わせで使用する場合、Vue.js は完全に洗練されたシングルページアプリケーションを提供することができます。 あなたが経験豊富なフロントエンド開発者で、 Vue.js を他のライブラリ/フレームワークと比較したい場合、他のフレームワークとの比較を チェックしてください。Vue.js で大規模アプリケーションを扱う方法に興味がある場合は、大規模アプリケーションの構築をチェックしてください。
AngularとReactに比べると学習コストは低く、というか多分Vue自体がjQueryの代替として考えられている気がしてその分低いと認識されている気がする。
テンプレート
htmlのテンプレート構文を利用していてバインドしているデータに変更があったら最小限のDOM再描画を行っている。 テンプレート構文を利用する点がPolymerと似ていたりして、JSXとに比べて普段htmlを 書いている人からしたら親しみやすいのかと思う。オプションでJSXを利用することもできる。
ReactとVueは似ているけど描画のロジックは根本的に違っているようで、Reactの方は際の DOM がどのような状態にするためにメモリ内の表現で仮想 DOM を活用し、状態を変更するとき、React は仮想 DOM の完全な再レンダリングを行い、その差分を求めて、そして実際の DOM にパッチする。 仮想 DOM の代わりに、Vue.js はテンプレートとして実在する DOM を使用し、データバインディングに対して実在するノードに参照を保ちます。そのためかVue.jsは性能のチューニングをほとんど必要としないらしい。
テスト
公式はKarmaを使うことを勧めている。
所感
jQueryの代替品を探しているのであればこれがベストなきがする。
結論
一般的な業務でSPAを扱うということがあるのであれば公式のサポートが手厚いAngularがよさそうに思います。jQueryの代替品を探しているレベルであればVueで手軽に実装するのが良さそう。データ操作周りで複雑なことをして見せたいページを作りたいのであればReactが良さそうな感じでしょうか。
DDD概要
社内の勉強会に備えてのメモ
DDDとは何か?
エリック・エヴァンスがソフトウェアのドメインモデリングと設計についてオブジェクトコミュニティの底流として現れた哲学をドメイン駆動設計と呼称し書籍にまとめた。
DDD(Domain-Driven Design) とはどのように設計していくかという理論、考え方、概念を指している。
具体的な設計・開発方法を指して言って入るわけではないのでわかりづらい点も多々あるかもしれない。
エヴァンス本の参考文献からもDDDは以下の書籍によって支えられて入ることがわかる
・XPエクストリープ プログラミング入門
・テスト駆動開発入門
・アナリシスパターン
・実践UML
・オブジェクト指向入門
他にもたくさんあって、DDDはこれらの書籍によって支えられている。
DDD(エヴァンス本)の目次
エヴァンス本の目次一覧をまとめてみる
第1部 ドメインモデルを機能させる
・第1章 知識をかみくだく
・第2章 コミュニケーションと言語の使い方
・第3章 モデルと実装を結びつける
第1章では設計者とドメインエキスパートでのコミュニケーションによりどうやって深いモデルを見つけていくかについて書かれている。そのためにもドキュメントを作ったり言葉を統一することによりコミュニケーションを取る方法を提示している。第3章では知識をかみくだいてえたモデルをコーディングに落とし込む手法としてモデル駆動設計に触れている。
第2部 モデル駆動設計の構成要素
・第4章 ドメインを隔離する
・第5章 ソフトウェアで表現されたモデル
・第6章 ドメインオブジェクトのライフサイクル
・第7章 言語を使用する:応用例
第2部ではモデルを実装に落とし込むモデル駆動設計について述べている。ドメインの設計をソフトウェアシステムにおけるその他の関心ごとから分離することで設計とモデルとのつながりを明確にする目的がある。そのためにも"第4章ドメインを隔離する"ではUI層、アプリケーション層、ドメイン層、インフラストラクチャ層のレイヤ化アーキテクチャについて説明している。"第5章ソフトウェアで表現されたモデル"ではモデルの実装部分であるドメイン層の構成要素のエンティティ、値オブジェクト、サービスについて説明している。それから2つのモデルを適切に分割(モジュール化)することによるメリットを述べている。"第6章ドメインオブジェクトのライフサイクル"ではエンティティ、値オブジェクトの集約について述べている。例えば自動車のドメインであったら集約ルートが自動車でそれに紐づいてタイヤエンティティ、車輪エンティティ、位置値オブジェクトが存在する。集約全体のオブジェクト精製方法についてファクトリパターンの説明がある。集約のルートがオブジェクトの永続化と永続化されたオブジェクトの検索に使用するリポジトリにも触れている。
第2部は実装寄りの話でここを理解できていればDDDっぽい実装ができるようになりそう。
第3部 より深い洞察へ向かうリファクタリング
・第8章 ブレイクスルー
・第9章 暗黙的な概念を明示する
・第10章 しなやかな設計
・第11章 アナリシスパターンを適用する
・第12章 デザインパターンをモデルに関係づける
・第13章 より深い洞察へ向かうリファクタリング
第2部ではモデルを実装を一致させる基礎の方法を学び、第3部ではドメインについてさらに深いモデルを開発するために必要不可欠となるリファクタリングを学ぶ。"第8章ブレイクスルー"は最初に思い描いたモデルで開発を進めた後、モデル自体は悪くなさそうであっても不具合が生じることがあって、そういうのは普段からの小さいリファクタリングによりモデルを明確化することである時問題が明確化して解決の糸口になるという話。"第9章暗黙的な概念を明示的にする"ではブレイクスルーを引き起こすための日々のリファクタリングとしてそれまで気づいていなか明白でない概念を見つける方法について書いている。"第10章しなやかな設計"では変更に強いしなやか設計はどうすれば行えるかについて書いてある。第11章と第12章ではアナリシスパターンとデザインパターンの利用方法について書いてある。"第13章より深い洞察へ向かうリファクタリング"ではどこからリファクタリングを始めるかについて書いてある。
第4部 戦略的設計
・第14章 モデルの生合成を維持する
・第15章 蒸留
・第16章 大規模な構造
・第17章 戦略をまとめ上げる
第4部ではモデルの生合成を維持していくための戦略的設計について述べている。"第14章モデルの生合成を維持する"では、コンテキストマップを描きコンテキストを境界づけておき、コンテキスト間の関係を明示的にしておく。"第15章蒸留"ではコンテキストマップを描いてコンテキスト間を境界づける際に各ドメインの役割となるコアドメインや汎用サブドメインなどの種類を学ぶ。"第16章大規模な構造"では巨大なシステムに包括的な原則を持たせることで設計全体にまたがるパターンにおいてどのような役割を果たすかという観点を分かりやすくする方法を学ぶ。
全17章でソフトウェアの上流部分からリファクタリング、成長までをまとめた長編になって入るのがわかる。DDD自体は具体的な実装についてあまり触れられていないので、実装にどう活かすかを学びたいのであれば実践ドメイン駆動設計を読んだり、サンプルのプロジェクトを調べるのが良さそう