SpringBootでWebSocketを使ってみたい(Sample Backend編)
- はじめに
- Keywords
- クラス構成
- GreetingController
- WebSocketConfig
- AbstractWebSocketMessageBrokerConfigurer
- おわりに
- 参照
はじめに
チャットのように、Webサーバーを通してデータの受け渡しをリアルタイムで実施するための技術にWebSocketがあります。
今回はこれをSpringBootから利用する方法として紹介されていた下記の内容について調べつつ、
いじってみたりすることにします。
Keywords
WebSocket
まずWebSocketについてですが、超乱雑にまとめると
- 最初にクライアント側とサーバー側を連携し、その後は接続・切断をせず小さいオーバーヘッドでやり取りを行う
- クライアント側からだけでなくサーバー側からもデータ(メッセージ)のやりとりをリクエストできる
といったところでしょうか。
頻繁なメッセージのやりとりが必要なチャットなどを作るときに利用されます。
詳しくは下記のようなページをご覧ください。(内容が間違っていたり、あまりにも説明が不足していることがわかれば後日修正します)
- Windows 8 のネットワーク接続 - Windows 8 と WebSocket プロトコル - MSDN
- 26. WebSocket Support - Spring
- RFC 6455 - The WebSocket Protocol (日本語訳)
- WebSocketについて調べてみた。 - Nao Minami’s Blog
今回のサンプルではsockjsを利用しています。
STOMP
WebSocketでクライアント側とサーバー側でやりとりをするためには、
お互いに共通した方法(プロトコル)を持っている必要があります。
サンプルでは、このためのプロトコルとしてSTOMP(Simple (or Streaming) Text Orientated Messaging Protocol)を使用しています。
正式名称の通り、テキストベースのメッセージでやりとりを行います。
メッセージのやりとりの流れは最初にSubscribeを行い、
更新があればそれを受け取る、ということのようです。
このSTOMPをWebSocket上で使用するため、STOMP.jsを利用しています。
一点気がかりなのは、GitHubを見るとメンテナンスが止まってしまっていること。
みなさんどう対処しているのか(´・ω・`)
。。。とりあえず今回はStomp.jsを使用することとします。
クラス構成
上記のサンプルに含まれるクラスは以下の通りです。
- src
- main.java.hello
この内「Application.java」はSpringBootの開始を担うメインクラス、「Greeting.java」「HelloMessage.java」はデータを保持するモデルクラスです。
ここからは「GreetingController.java」「WebSocketConfig.java」について追いかけてみます。
GreetingController
コントローラークラスです。
GreetingController.java
import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; @Controller public class GreetingController { @MessageMapping("/hello") @SendTo("/topic/greetings") public Greeting greeting(HelloMessage message) throws Exception { // 1秒待ってから応答する. Thread.sleep(1000); // メッセージ内容を"/topic/greetings"のSubscriberに渡す. return new Greeting("Hello, " + message.getName() + "!"); } }
- @Controllerアノテーションを付与することで、通常のルーティング処理に加えてメッセージのルーティングも行うことができます。
- @MessageMappingアノテーションを付与することで、今回は「/hello」にメッセージが送信されたときに関数「greeting(HelloMessage message)」が呼ばれます。
このときメッセージの内容は、「HelloMessage.java」に格納された形で引数として渡されます。 - @SendToアノテーションを付与することで、戻り値であるメッセージの内容を「/topic/greetings」をSubscribeしているユーザーに伝えることができます。
- 「Thread.sleep(1000);」は無くても動作します。
なおマッピングできるパスの形式は、「/hello」の他「/hello.message.*」や「/hello.message.{goodmorning}」といったものも指定できるようです(MessageMapping.classより)。
WebSocketConfig
設定クラスです。
WebSocketConfig.java
import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/gs-guide-websocket").withSockJS(); } }
- @EnableWebSocketMessageBrokerアノテーションを付与することで、WebSocketのメッセージのやり取りを有効にします。
ここで実装している内容は2つです。
- MessageBrokerの設定
- STOMPのエンドポイントの登録
1. MessageBrokerの設定
メッセージのやりとりを仲介するMessageBrokerの設定を行います。
config.enableSimpleBroker(“/topic”);
「/topic」以下のURL(GreetingController.javaのメッセージ送信先である「/topic/greetings」)でメッセージを渡せるようにします。
メッセージを受け取ったら、あらかじめSubscribeしていたユーザーにメッセージが渡されます。
config.setApplicationDestinationPrefixes(“/app”);
「/app」以下のURLにメッセージを送ると、コントローラークラス(GreetingController.java)が呼ばれるようになります。
今回はクライアント側から「/app/hello」にメッセージを送信すると、
GreetingController.javaの「greeting(HelloMessage message)」が呼ばれます。
2. STOMPのエンドポイントの登録
sockjsを使ってWebSocketの接続処理を行うエンドポイントの登録を行います。
「withSockJS()」は今回sockjsを使っているため必要なのですが、
sockjsを使わない場合にWebSocketを使う処理が行えるのか、可能であればどのようにするのかは気になるところです。
AbstractWebSocketMessageBrokerConfigurer
設定クラス(WebSocketConfig.java)で継承しているAbstractWebSocketMessageBrokerConfigurerではどのようなことをしているのか、少し見てみることにしました。
AbstractWebSocketMessageBrokerConfigurer.java
package org.springframework.web.socket.config.annotation; import java.util.List; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; public abstract class AbstractWebSocketMessageBrokerConfigurer implements WebSocketMessageBrokerConfigurer { @Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { } @Override public void configureClientInboundChannel(ChannelRegistration registration) { } @Override public void configureClientOutboundChannel(ChannelRegistration registration) { } @Override public boolean configureMessageConverters(ListmessageConverters) { return true; } @Override public void addArgumentResolvers(List argumentResolvers) { } @Override public void addReturnValueHandlers(List returnValueHandlers) { } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { } }
※コメント、Javadocsは省略しています。
WebSocketMessageBrokerConfigurer.javaのメソッドをOverrideしていますが、
中身はすべて空になっています。
ドキュメントを見ると、WebSocketMessageBrokerConfigurer.javaを実装しやすくするため、
オプショナルな関数を空で実装している抽象クラスのようです。
設定クラスで実装していた「configureMessageBroker(MessageBrokerRegistry registry)」も含まれているため、
実装が必須な関数というのは、STOMPのエンドポイントの登録を行う「registerStompEndpoints(StompEndpointRegistry registry)」だけのようですね。
では、WebSocketMessageBrokerConfigurer.javaも見てみます。
WebSocketMessageBrokerConfigurer.java
package org.springframework.web.socket.config.annotation; import java.util.List; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; public interface WebSocketMessageBrokerConfigurer { void registerStompEndpoints(StompEndpointRegistry registry); void configureWebSocketTransport(WebSocketTransportRegistration registry); void configureClientInboundChannel(ChannelRegistration registration); void configureClientOutboundChannel(ChannelRegistration registration); void addArgumentResolvers(ListargumentResolvers); void addReturnValueHandlers(List returnValueHandlers); boolean configureMessageConverters(List messageConverters); void configureMessageBroker(MessageBrokerRegistry registry); }
※コメント、Javadocsは省略しています。
オプショナルな関数の内容は下記を参照していただきたいところですが、
WebSocketでのメッセージの送受信を行う関数(MessageChannel)の設定などが行えるため、必要に応じてOverrideすることになりそうです。
なお、interfaceで共通の関数を指定して、共通の処理を抽象クラスで実装し、個別の処理を抽象クラスを継承したクラスで実装する、
という流れはまさにJava本格入門で読んだ内容だな〜、などと思いながら見ていました。
おわりに
細かく追ってはみたつもりですが、全体的にまだフワフワしているため、
実際にサンプルを作りながらもう少し突っ込んで触る必要がありそうです。
次回はクライアント側を追いかけてみます。
参照
Spring
- Getting Started · Using WebSocket to build an interactive web application - Spring
- Spring 4.3 WebSocket関連の主な変更点(+簡易アプリ作成!!) - Qiita
- 26. WebSocket Support - Spring
- Spring Bootでチャットツールを作りながらWebの仕組みを理解しよう! - SlideShare
- 6. STOMPを使ってみる — Spring Bootキャンプ ハンズオン資料 1.0.0-SNAPSHOT ドキュメント
WebSocket
- Windows 8 のネットワーク接続 - Windows 8 と WebSocket プロトコル - MSDN
- RFC 6455 - The WebSocket Protocol (日本語訳)
- WebSocketについて調べてみた。 - Nao Minami’s Blog
- SockJS-client - sockjs - GitHub
STOMP
UbuntuのUnity上でUnity3Dを動かす
はじめに
以前Ubuntu向けのUnity3D(デスクトップ環境と混ざってややこしいので今回はゲームエンジンの方はUnity 3D表記とします)のEditorが開発されている、
という話は聞いていたのですが、その後どうなったのかなぁ〜と思ったらちゃんと開発が続いていました。
というわけで、Unity 3Dと、スクリプトを書くためのJetBrains Riderをインストールした話をまとめます。
※2017年4月現在、Ubuntu用のUnity 3Dはあくまで開発版であるため、予期せぬバグなどが存在する恐れがあります。
準備
gdebi
後述しますが、Ubuntu用のUnity 3Dは.debファイルとして入手できます。
この.debファイルをインストールするときに、依存するパッケージが不足している場合に自動でインストールしてくれる「gdebi」をインストールします。
sudo apt install gdebi
【参考】
- » ローカルでdebパッケージをインストールするとき依存関係も解決してくれる「gdebi」 - unskilled
Nodebrew
Unity 3DのインストールにはNode.jsが必要になります。
Angularなどの開発のため、新しいバージョンのものを使いたかったのでNodebrewをインストールし、
ver.7.9.0をインストールしました。
(Node.jsのインストールは延々ログが出力されるのと、ちょっと時間がかかったため不安になりましたが、無事一発でインストールできました)
インストールが終わったら、使用するNode.jsとして、インストールしたver.7.9.0を指定しておきます。
nodebrew use v7.9.0
【参考】
その他
後述しますが、Riderを使うためにはMono ver.4.4以上が必要なためインストールするのですが、
Unity 3Dをインストールする前に入れておくと、古いパッケージをインストールするのを防げるかもしれません。
Unity 3Dのインストール
Ubuntu用のUnity 3Dの.debファイルのリンクはここにまとめられているようです。
(2017年4月現在の)最新版はなぜか下から2番目にある5.6.0f3だと思いますので、
これをダウンロードし、下記のコマンドでインストールします。
sudo gdebi ダウンロードしてきた.debファイル
エラーが無くインストールが終われば完了です。
WindowsやMac用と同じように、ログインやライセンスの選択を行います。
起動後もWindowsやMac用と同じように使用可能ですが、何故かScene上でカメラを回転させる操作が、
左クリックではなく右クリックなのはちょっと違和感が。。。
Riderをインストールする
RiderはJetBrainsのサイトからEAP版をダウンロードして任意の場所に展開します。
Linux版はインストーラーが付属しているのではなく、binフォルダ内にあるrider.shというファイルをターミナルで実行することでRiderが起動します。
よ〜し、じゃあ試しに何か作ってみるかぁ〜、と思ったところ。。。
エラーでRiderが起動しませんでしたorz
Riderでプロジェクトを作成した場合も、下記のようなエラーが出てソリューションファイルの読み込みができませんでした。
Solution 'New Unity Project' load failed: MsBuild not found on this machine
Monoのインストール
エラーでググったところ、どうやらバージョン4.4以上のMonoがインストールされていないのが原因とのこと。
ただ、そのまま「apt install mono-runtime」としてしまうとそれより古いバージョンがインストールされてしまうため、
公式を参考にリポジトリを追加してインストールします。
インストールするのは、「mono-runtime」「mono-devel」「mono-complete」の3つです。
sudo apt install mono-runtime mono-devel mono-complete
【参考】
- Install Mono on Linux - Mono
- JetBrains Rider EAP on Linux - Solution load failed: MsBuild not found - Stack Overflow
- Project Rider system requirements – JetBrains Rider Support
これで勝ったな、ガハハ
おわりに
Monoのあたりでちょっと手間取ったものの、割とすんなりインストールできてよかったです。
これでいよいよどのOSを選んでも不便なく開発できるようになってきましたね(๑•̀ㅂ•́)و✧
ちなみにタイトルにまでしたUbuntuのUnity上でUnity 3Dを動かす、というのは、
何故かデスクトップ環境をUnity8にしたところ、ウインドウが謎の点滅を起こしたためわずか数分で終了しましたorz
やったー、Unity3D on Unityが実現した😀
— 🍅増井将則(Nullable) (@masanori_msl) 2017年4月25日
あとはJetBrains Riderのインストールやな。 pic.twitter.com/0utL670LQn
まぁGnome3上では快適ですので。。。^^;
AngularのプロジェクトをビルドしてGolangで動かす
はじめに
以前作成したAngularのプロジェクトをビルドして、 Golangを使って表示してみます。
Angularのプロジェクトをビルドする
実は最近のJS系の勉強会などに参加していて気になっていたことがあります。
「本番ではどうやって表示するの?」
例えばバックエンドをSpring Bootで構築したとして、
フレームワークを使わずに作った場合はSpring Bootのプロジェクトに組み込んで、
Thymeleafなどを使って表示するかと思います。
しかし、例えばAngularの場合だとAngular-cliを使って、
Angularのプロジェクト単体で表示します(確認の段階では)。
では、それぞれを完全に別に扱うの…?
などなど考えていたのですが、実際にはAngularのプロジェクトをビルドすることで、
上の例であればSpring Bootのプロジェクトに組み込むことができる、ということでした。
ビルド自体は(とりあえずビルドするだけなら)簡単で、Angularのプロジェクト直下で下記を実行するだけです。
ng build
ビルドに成功すればプロジェクト直下に「dist」というフォルダが作成され、
必要なファイルがその中に出力されます。
あとはバックエンド側のプロジェクトにまとめて放り込んで、
「index.html」を表示してあげればOKです。
今回はGolangのファイル(webtest.go)と同じディレクトリに「templates」というディレクトリを作り、
その中に全てファイルを保存しました。
HTMLファイルを読み込む
何故か我が家にある(買った)Go言語によるWebアプリケーション開発で扱われているコードを参考にHTMLを表示してみます。
https://github.com/matryer/goblueprints/blob/master/chapter1/chat/main.go
webtest.go
package main import ( "html/template" "log" "net/http" "path/filepath" "sync" ) type templateHandler struct { once sync.Once filename string viewTemplate *template.Template } func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { t.once.Do(func() { t.viewTemplate = template.Must(template.ParseFiles(filepath.Join("templates", t.filename))) }) t.viewTemplate.Execute(w, nil) } func main() { http.Handle("/", &templateHandler{filename: "index.html"}) // Portを指定してWebサーバーを開始. if err := http.ListenAndServe(":8088", nil); err != nil { // エラーが発生すればログ出力. log.Fatal("ListenAndServe:", err) } }
http.Handle, ServeHTTP
ファイル読み込みのキーになる部分がhttp.Handle、ServeHTTPです。
http.Handle
第一引数に対象とするURL(上記ではルートディレクトリ)を、
第二引数にhttp.Handlerという関数ServeHTTPを持つインターフェースを取ります。
で、対象URLにアクセスした時、Spring bootでいう「@RequestMapping」のように、
第二引数から関数ServeHTTPが実行され、「index.html」が表示される、という流れになっています。
ServeHTTP
ここで気になるところとして、上記では第二引数としてhttp.Handlerではなく、構造体を渡しています。
これがエラーにならないのは、
Golangでは構造体があるインターフェース(ここではhttp.Handler)が持つ関数を全て持っている場合、
そのインターフェースと同じものとして扱うことができるためです。
なお構造体に関数を持たせているのがServeHTTPの「(t *templateHandler)」の部分です。
便利ですが、なかなか慣れないとナンノコッチャ分からんですね。。。(;・∀・)
その他の詳しい解説は本を参照。
…なのですが、この状態ではLoading…の文字しか表示されません。
index.htmlから呼び出しているJavascriptにアクセスできないためです。
また同じ理由でFaviconも表示できません。
静的ファイルを読み込む
これらのファイルにアクセスできるようにするためにはhttp.Handleを使います。
webtest.go
// Javascriptのファイルを読み込む. http.Handle("/js/", http.StripPrefix("/js/", http.FileServer(http.Dir("templates")))) // 画像ファイルを読み込む. http.Handle("/img/", http.StripPrefix("/img/", http.FileServer(http.Dir("templates"))))
そして、ng buildで出力したindex.htmlを少し変更します。
MainPage.html
< !doctype html> < html> < head> < meta charset="utf-8"> < title>NgCustomer< /title> < base href="/"> < meta name="viewport" content="width=device-width, initial-scale=1"> < link rel="icon" type="image/x-icon" href="img/favicon.ico"> < /head> < body> < app-root>Loading...< /app-root> < script type="text/javascript" src="js/inline.bundle.js">< /script> < script type="text/javascript" src="js/polyfills.bundle.js">< /script> < script type="text/javascript" src="js/styles.bundle.js">< /script> < script type="text/javascript" src="js/vendor.bundle.js">< /script> < script type="text/javascript" src="js/main.bundle.js">< /script> < /body> < /html>
変更の理由は、http.Handleの第一引数で指定するURLが、index.htmlと重複するのを避けるためです。
第二引数「http.StripPrefix(“/js/”, http.FileServer(http.Dir(“templates”)))」は、
(例えば)アクセスされるURL「/js/inline.bundle.js」から「/js/」の部分を取り除き、
templatesディレクトリ内にあるinline.bundle.jsを取得します。
とりあえず、これでAngularのファイルを表示できるようになりました。
まだ怪しい要素もありますが、抜けなどがあれば追加・修正します。
他からアクセスできるようにする
確認の際は「go run webtest.go」を実行していますが、
例の如く?他のマシンからこのページにアクセスすることはできません。
どうするかというと、これも例によってオプションを追加します。
go run webtest.go -serve 0.0.0.0
参照
Angular
Golang
GolangでFizzBuzzしてみた
はじめに
ひょんなことから(Twitterで見た)4月にGolangの勉強会に参加することになりました。 いくら話を聴くだけとはいえ、何もわからずに参加しても楽しくないよなぁ…。
ということでちょっと触ってみることにしました。
何を作ってみるか?というところなのですが、
実は「プログラミングの基本」のような扱いを受けるFizzBuzzってやったことがなかったので、Golangでやってみることにしました。
念の為FizzBuzzのルールですが、
というルールで進めます。
インストール
Ubuntuではaptからインストールできるのですが、これは少しバージョンが古い(ver.1.6)です。
最新のver.1.8をインストールするには、下記のような方法がありそうです。
ただ、ググった中で今ひとつスタンダードなやり方が定まっていない気がしたため、1のtar.gzを使うことにしました。
インストール方法は下記を参照。
https://golang.org/doc/install
一点、/usr/localにファイルを置く下記の部分で権限エラーになったため、sudoで実行しました。
tar -C /usr/local -xzf go$VERSION.$OS-$ARCH.tar.gz
「go version」でバージョン情報が表示されたらOKです。
エディタはIntelliJ IDEAと迷いましたが、 Angularで使っていたこともありVisual Studio CodeにGolangのプラグインをインストールして使っています。
通常のFizzBuzz
ではさっそくFizzBuzzってみましょう。
まずは至って普通の方法から。
package main import "fmt" func main() { useFor() } func useFor() { for i := 1; i <= 100; i++ { if i%15 == 0 { fmt.Println("FizzBuzz") } else if i%3 == 0 { fmt.Println("Fizz") } else if i%5 == 0 { fmt.Println("Buzz") } else { fmt.Println(i) } } }
これを「go run ファイル名」で実行すればOKです。
簡単簡単。。。
}の位置にハマる
orz
✗ 間違い
if i%15 == 0 { fmt.Println("FizzBuzz") } else if i%3 == 0 { fmt.Println("Fizz") }
○ 正しい
if i%15 == 0 { fmt.Println("FizzBuzz") } else if i%3 == 0 { fmt.Println("Fizz") }
「}」と「else if{」が同じ行にないと、「'syntax error: unexpected else, expecting }‘」とエラーになります。
厳しい…
rangeを使う
さて、For文には配列を作ってrangeを使う方法もあります。
package main import "fmt" func main() { useRange() } func useRange() { for _, v := range getNumArray(1, 100) { if v%15 == 0 { fmt.Println("FizzBuzz") } else if v%3 == 0 { fmt.Println("Fizz") } else if v%5 == 0 { fmt.Println("Buzz") } else { fmt.Println(v) } } } func getNumArray(fromNum int, toNum int) []int { // fromNumからtoNumまでの配列(Slice)を作る. resultArray := make([]int, toNum-fromNum+1) for i := 0; i < len(resultArray); i++ { resultArray[i] = fromNum + i } return resultArray }
range
For文で使用し、配列(今回は[]int)を渡すことでIndexと値を取得できます。
今回は使用しませんが、Indexが必要な場合は「for _, v := 〜」の部分を「for i, v := 〜」のように変更します。
「for i := 〜」のように書くとIndexのみが取得でき、
「i」にはIndexが、「v」には値が格納されます。
「_」のように書いているのは、Goでは未使用の変数があるとエラーになってしまうためそれを避けるためです。
slice
「getNumArray」では、1から100までの数値の配列を作っています。
Rubyのように「(1..100)」とかできると楽だったのですが。
配列には「[3]int」のように要素数を指定したものと、そうでないSliceとがあります。
要素数を指定する場合、「[3]int」と「[5]int」が別の型として扱われるため、
要素を指定する必要のないSliceを使っています。
Sliceのインスタンスを初期化する場合、「make(型, 要素数)」を使うことができます。
RxGoを使う
例えばC#でFizzBuzzをする場合、Linqを使ってFor文は使わずにFizzBuzzすることができます。
しかし、残念ながら?Goには標準でそのような機能が用意されていないようです。
で、ふと思いだしたのがRxJavaで、StreamAPI的な処理ができるっていってたな〜、と。
↓
ということは、GoにもRxが用意されていれば、同じようなことできるんじゃね?
と思って調べてみたら、そのままズバリRxGoがありました。
https://github.com/ReactiveX/RxGo
そんなこんなでRxGoを使ってFizzBuzzしてみます。
go get
RxGoはGolang標準の機能ではないため、ファイルをインストールしてやる必要があります。
そのために行うのがgo getです。
READMEにある通り下記をターミナルで実行するとインストールできます。
go get -u github.com/reactivex/rxgo
今回はobservable.Range(さっきとは違うやつです)を使ってみました。
package main import ( "fmt" "github.com/reactivex/rxgo/handlers" "github.com/reactivex/rxgo/observable" ) func main() { useRx() } func useRx() { stream := observable.Range(1, 101) wait := stream.Subscribe(handlers.NextFunc(func(item interface{}) { if v, ok := item.(int); ok { if v%15 == 0 { fmt.Println("FizzBuzz") } else if v%3 == 0 { fmt.Println("Fizz") } else if v%5 == 0 { fmt.Println("Buzz") } else { fmt.Println(v) } } else { fmt.Println("Invalid value") } })) <-wait }
- 「observable.Range(1, 101)」で1から100まで(101は含まない)繰り返し実行するためのStreamを生成します。
- 「wait」の型は「channel」で、非同期で実行される処理の完了を待ちます。
- RxGoのWikiでは「handlers.NextFunc」の引数である「func(item interface{})」に型は指定されていないのですが、
少なくともver.1.8ではエラーとなるため、型を指定しています。 - 引数の型を「interface{}」とすることで、型を指定せずに引数を受け取ることができます。
- 「if v, ok := item.(int); ok {」の行で引数の型を確認し、intであった場合は「v」に格納された値を使ってFizzBuzzします。
おわりに
これまでC#やKotlinをはじめ多機能な言語ばかりを触っていたため、あれもないこれもない、おまけにフォーマットや型に厳しい(´・ω・`)
というのがGolangに対する正直な感想です。
ただ、「Goに入ってはGoに従え」という言葉もありますし、これまでの言語のやり方が完璧か?と言われればそうではない気がするので、
Golangが別の方向に進化する、というのは興味深いと思います。
一点、言語的にシンプルだったり独自路線なのは良いと思うのですが、
その先に幸せが待っている、と信じさせる何かがないと結局各々が独自にフレームワークやオレオレ実装プラグインを持ちだして大変になるのでは…?
という危惧はあります。
まぁ素人考えなので、ちゃんとその辺りも考慮して進められているのかとは思いますが。
参照
- Documentation - The Go Programming Language
- Reactive Extensions for the Go language. - ReactiveX/RxGo - GitHub
- Golang の channel の使い所 - Big Sky
- インターフェース - お気楽 Go 言語プログラミング入門
- 急いで学ぶGo lang#3 まずは基本構文 - Developers.IO
- 急いで学ぶGo lang#4 関数・ポインタ・制御構文 - Developers.IO
- 急いで学ぶGo lang#6 インターフェイス - Developers.IO
- 急いで学ぶGo lang#7 range・Array・slice・map - Developers.IO
- Go言語のchannelって一体何よ ~基礎編~【golang】 - DRYな備忘録
- interface{} な変数を型が決まっている関数の引数にする - Qiita
ng-kyoto Angular Meetup#5で発表してきました
はじめに
3/20に行われたng-kyoto Angular Meetup #5で発表してきました。
https://masanori840816.github.io/ngkyoto5/slidepage.html
振り返って
いやぁ、緊張しました(苦笑)。
(完全に言い訳ですが)いつも以上に時間が取れず、
コードの解説にしてもどこを話すか、どこを飛ばすかがまとめきれていなかったと思います。
あと舞い上がりすぎて、今自分がどのぐらいの時間話しているかがわからなくなったり、
PCとスクリーンの画面の位置が逆になっていて、カーソルがうまくスクリーン側に移動できなかった、というのも反省点です。
現在の経過時間については、ストップウォッチアプリを探すか自分で作るかして、
自分のディスプレイで確認できるようにしておくようにします。
ディスプレイの設定(解像度やおそらく配置も)は、設定 > ディスプレイからできるようなので、
次回はちゃんと設定して臨みたいと思います。
いつもながら温かく見守ってくれた皆様には感謝感謝です。
内容について
基本的にはここ最近の記事の内容をまとめた感じになっています。
(というかそのために調べた結果を記事にしていました)
この中でも積み残し課題としていたアニメーションの停止について。
そもそもAngularではアニメーションクラスの元になっているCSSアニメーションを直接書くことも可能、
ということなので、そちらで解決するのも良さそうです。
アニメーションクラスで簡単にまとめて書ける強みもあるので、状況によって切り分ける感じでしょうか。
ただ、アニメーションクラスを使わないとできない表現として、
URLを変えつつ、該当のページがアニメーションで移動してくる、といった表現をする場合が挙げられました。
これはちょっと実装が大変そうではありますが、近いうちに試してみたいところです。
他の方の発表について
Angular自体の話では、Anglar-cli、ver.4.0、DIとところどころ理解が追いつかない部分もありましたが、
どれも今後試してみたい内容でした。
※なお発表を聴いて、こっそりブログ記事の「Angular.js」を「Angular」に直したのは内緒です
ということで、フレームワーク選定のお話でも挙げられたVue.jsやReactが気にはなりつつも、
フロントについてはもう少し続けてAngularを進めたいと思います。
業務で触っていれば良いのですが、現状触れるのがプライベートのみなので…。
WebComponentについてはただただすごいなぁ〜、と思うばかりでした(苦笑)。
まぁ今のJavascriptやHTMLに取って代わるもの、というわけでもないようで、
ChromeやFirefoxでの対応もあったということですし、ニュースなどは追いかけるようにしようかな、と思いました。
おわりに
ということで、最初から最後まで楽しい勉強会でした。
次回はver.5.0が出る頃、ということなので、
今回はアニメーションに関連する部分に絞っていましたが、もう少し基本的なところから触っていって、
もし機会があればまた発表にも挑戦してみたいと思います。
主催の皆様、会場を提供していただいたはてな様、参加の皆様、ありがとうございましたm( )m
おまけ
スライドにはRemark.jsを使ってGitHub Pagesを使っているのですが、
スライド資料として認識されないのは少し不便ですね。。。
MarkDown形式で書くだけでそれらしく表示されるため便利ではあるのですが。
AngularのAnimate2
はじめに
前回の続きです。
公式ドキュメントのボタンのように、マウスオーバーした時にボタンが浮き上がって見える、という動きを真似してみます。
マウスオーバーとマウスが離れた時のイベントを取得する
まずはマウスカーソルをボタン上に移動させた場合(マウスオーバー)と、ボタン外に移動させた場合(マウスアウト)とでイベントを取得します。
gui-anime.component.html
< button [@positionState]='state' (mouseover)="onButtonIn()" (mouseout)="onButtonOut()"> タイトル < /button>
gui-anime.component.ts
〜省略〜 export class GuiAnimeComponent implements OnInit { private state: string; constructor() { } ngOnInit() { } private onButtonIn(){ this.state = 'active'; } private onButtonOut(){ this.state = 'inactive'; } }
CSS
影や位置のしていなど、アニメーションで切り替えたい要素以外の装飾については、angular-cliで「ng g component 〜」実行時にhtmlやtsなどと同時に生成されるcssに書くことにします。
gui-anime.component.css
.positionanime{ background-color: #fff; border-radius: 8px; width:20vw; height: 10vh; position: relative; -webkit-border-radius: 8px; -moz-border-radius: 8px; margin: 10px; }
ボタンの背景色、角丸設定などを行っています。
アニメーション
それでは、アニメーション部分にとりかかります。
今回主に実装するのは下記の動きです。
- 枠の太さ、色を非アクティブ・アクティブな時で切り替える
- ボタンが非アクティブなときは小さめの影を、アクティブなときは大きめの影を表示する
- ボタンを主にZ方向に移動させる
枠
枠の色や太さを指定します。
gui-anime.component.ts
import { Component, Input, trigger, state, style, transition, animate, AnimationPlayer, OnInit } from '@angular/core'; @Component({ selector: 'app-gui-anime', templateUrl: './gui-anime.component.html', styleUrls: ['./gui-anime.component.css'], animations: [ trigger('positionState', [ state('active', style({ border: '3px solid #2196F3', boxShadow: '0px 5px 6px 3px #CECECE', transform: 'translate3d(-1px, -1px, 50px) scale(1.02)', zIndex: 30 })), state('inactive', style({ border: '1px solid #CECECE', boxShadow: '0px 1px 3px 1px #CECECE', transform: 'translate3d(0px, 0px, 0px) scale(1)', zIndex: 10 })), transition('active => inactive', animate('200ms ease-in-out')), transition('inactive => active', animate('200ms ease-in-out')) ]) ] }) 〜省略〜
- 枠の太さの指定方法として、px指定の他にも「thin」や「medium」などを使うこともできます。
が、今回のようにアニメーションさせる場合は一瞬枠が指定より太くなるなどの問題が発生するため、pxなどで指定しておいた方が良さそうです。
影
影は「boxShadow」で指定します。
gui-anime.component.ts
〜省略〜 animations: [ trigger('positionState', [ state('active', style({ border: '3px solid #2196F3', boxShadow: '0px 5px 6px 3px #CECECE', transform: 'translate3d(-1px, -1px, 50px) scale(1.02)', zIndex: 30 })), state('inactive', style({ border: '1px solid #CECECE', boxShadow: '0px 1px 3px 1px #CECECE', transform: 'translate3d(0px, 0px, 0px) scale(1)', zIndex: 10 })), transition('active => inactive', animate('200ms ease-in-out')), transition('inactive => active', animate('200ms ease-in-out')) ]) ] }) 〜省略〜
- 指定する値は 1:影のX座標値 2:Y座標値 3:ブラー(値が大きいほど広くぼんやりした影になる) 4:影の大きさ 5:影の色 です。
移動
アニメーションで要素を移動させたい場合、「translate」を使用します。 X、Y、Zの特定方向のみに移動させるときは「translateX」「translateY」「translateZ」、すべてを移動させる場合は「translate3d」を使います。
gui-anime.component.ts
〜省略〜 animations: [ trigger('positionState', [ state('active', style({ border: '3px solid #2196F3', boxShadow: '0px 5px 6px 3px #CECECE', transform: 'translate3d(-1px, -1px, 50px) scale(1.02)', zIndex: 30 })), state('inactive', style({ border: '1px solid #CECECE', boxShadow: '0px 1px 3px 1px #CECECE', transform: 'translate3d(0px, 0px, 0px) scale(1)', zIndex: 10 })), transition('active => inactive', animate('200ms ease-in-out')), transition('inactive => active', animate('200ms ease-in-out')) ]) ] }) 〜省略〜
- 上記では位置の指定をpxでしていますが、X、Yについては%を使って指定することもできます。
- translateとscaleなど、「transform」に複数の要素を指定したい場合、上記のようにそれぞれをスペース区切りで指定します。
Z-Index
今回は必要ないのですが、例えばボタン同士がひっついていて、マウスオーバー中のボタンを前に出したい、という時に、うまく該当のボタンが前に出てくれない場合があります。
そのような場合、「z-index」を使い、マウスオーバー時の値をそれ以外より大きく設定することで解決できます
(※CSSで「position: relative;」などの指定をする必要があります)。
gui-anime.component.ts
〜省略〜 animations: [ trigger('positionState', [ state('active', style({ border: '3px solid #2196F3', boxShadow: '0px 5px 6px 3px #CECECE', transform: 'translate3d(-1px, -1px, 50px) scale(1.02)', zIndex: 30 })), state('inactive', style({ border: '1px solid #CECECE', boxShadow: '0px 1px 3px 1px #CECECE', transform: 'translate3d(0px, 0px, 0px) scale(1)', zIndex: 10 })), transition('active => inactive', animate('200ms ease-in-out')), transition('inactive => active', animate('200ms ease-in-out')) ]) ] }) 〜省略〜
ngFor
さて、このボタンを1つだけではなく複数並べてみたいと思います。
ただし、全く同じ動作をするものをコピペでは作りたくない。。。
ということで、interfaceを作ってボタン表示に必要な要素を持つ配列を作り、「ngFor」を使って並べてみることにします。
interface
「ng g interface AnimeButton」をTerminalで実行して、interfaceを作成します。
anime-buton.ts
export interface AnimeButton { id: number; pageTitle: string; discription: string; state: string; }
で、Componentでこのinterfaceの配列を作ります。
gui-anime.component.ts
export class GuiPositionanimeComponent implements OnInit { private animeButtons: AnimeButton[] = [ {id: 0, pageTitle: 'Page1', discription: 'Page No.1', state: 'inactive'}, {id: 1, pageTitle: 'Page2', discription: 'Page No.2', state: 'inactive'}, {id: 2, pageTitle: 'Page3', discription: 'Page No.3', state: 'inactive'}, ]; constructor() { } ngOnInit() { } private onButtonIn(targetId: number){ this.animeButtons[targetId].state = "active"; } private onButtonOut(targetId: number){ this.animeButtons[targetId].state = "inactive"; } }
これをHTMLにセットします。
gui-anime.component.html
< section class='animeButtons'> < div *ngFor="let aButton of animeButtons"> < button class='positionanime' [@positionState]=aButton.state (mouseover)="onButtonIn(aButton.id)" (mouseout)="onButtonOut(aButton.id)"> {{aButton.pageTitle}} < /button> < /div> < /section>
- まず親となるタグで「class=‘animeButtons'」(「animeButtons」はComponentで定義した変数名)を指定し、
「*ngFor=“let aButton of animeButtons"」でC#のForeach文のように値を取り出しています。
これで比較的シンプルに複数ボタンが表示できます。
疑問点
ngForから値を取り出す部分で、「{{aButton.pageTitle}}」のように値を取り出すサンプルは見つかったのですが、 「[@positionState]=aButton.state」の「aButton.state」部分のような書き方は見つけられませんでした。
とりあえず動作はしているのですが、これで正しいのかが不安なところです。
これについては正誤が判断つき次第追記します。
参考
- Animations - ts - GUIDE - Angular
- ng2-hands-on-seed/courses/tutorial at master - ng-japan/ng2-hands-on-seed - GitHub
- 〜なんだかいけそうな気がする〜Angular入門 - Speaker Deck
- Angular 2 Cookbook
角丸
枠
影
移動
z-index
AngularのAnimate
はじめに
前回に続いてAngular.jsのお話。
昔Angular.jsの弱点としてアニメーションが挙げられていた気がしていたので(別のものと混同している恐れもあります)、
ちょこっと調べてみたところver.1.14位からちゃんと対応されていました。
というわけで今回は、公式ドキュメントを中心に試してみたことをメモることにします。
あ、ちなみに扱うのはver.2系だけです。
アニメーションさせてみる
まずはボタンをクリックした時に、ボタンのサイズ変更、及び背景色を変更するサンプルを試してみました。
前回作成したプロジェクトに、「ng g component gui-anime」でコンポーネントを追加し、それをあれこれいじっています。
まずはコードから。
Htmlにアニメーションのターゲット兼トリガーとなるボタンを追加し、
Typescript側でアニメーションとアニメーション開始のトリガーとなる要素(State)を作成します。
gui-anime.component.html
< p> gui-anime works! < /p> < button [@buttonState]="state" (click)="onAnimeButtonClicked()">Anime< /button>
gui-anime.component.ts
import { Component, Input, trigger, state, style, transition, animate, OnInit } from '@angular/core'; @Component({ selector: 'app-gui-anime', templateUrl: './gui-anime.component.html', styleUrls: ['./gui-anime.component.css'], animations: [ trigger('buttonState', [ state('inactive', style({ backgroundColor: '#eee', transform: 'scale(1)' })), state('active', style({ backgroundColor: '#cfd8dc', transform: 'scale(1.5)' })), transition('inactive => active', animate('100ms ease-in')), transition('active => inactive', animate('100ms ease-out')) ]) ] }) export class GuiAnimeComponent implements OnInit { public state: string; constructor() { } ngOnInit() { this.state = "inactive"; } private onAnimeButtonClicked(){ this.state = (this.state === "active")? "inactive": "active"; } }
イベント
Angularではクリックなどのイベントは()で囲まれます。
今回はボタンクリックのイベントを取得したいため、gui-anime.component.htmlで「(click)」を設定しています。
(click)="onAnimeButtonClicked()"
アニメーションのトリガーとなる要素(State)の指定と値の変更
上記のコードでは、クリックしたらアニメーションが始まるのではなく、gui-anime.component.tsで設定している「buttonState」が特定の値(inactive、active)に変更された場合に開始されるようになっています。
これをボタンに設定して、クリックイベントで「state」の中身を入れ替える、という処理を行っています。
[@buttonState]="state"
[@〜]とすることで、コンポーネントの持つ要素(今回はアニメーションのトリガーとなる「buttonState」)の「state」との関連付けができるようです。
[]は下記のPropertyBindingか?とも思うのですが、確信が持てず。。。
この辺りは追って調べてみることにします。
アニメーション
さて、今回のメインであるアニメーションです。
animations: [ trigger('buttonState', [ state('inactive', style({ backgroundColor: '#eee', transform: 'scale(1)' })), state('active', style({ backgroundColor: '#cfd8dc', transform: 'scale(1.5)' })), transition('inactive => active', animate('100ms ease-in')), transition('active => inactive', animate('100ms ease-out')) ]) ]
ここでは2つのstateが指定されており、
先ほど登場した「buttonState」の値が「inactive」または「active」(それぞれのstateの第一引数)に変更された時に、
第二引数でそれぞれ指定しているCSSの値(背景色、スケール値)が適用される内容となっています。
で、その後ろに連なる2つのtransitionによって、「inactive」から「active」またはその逆への変化がアニメーションで繋げられる、という処理が行われます。
transitionの第二引数にてアニメーションの時間と後述するEaseTypeが指定されています。
EaseType
transitionで指定されているEaseType。
これを指定することで、アニメーション(今回は拡大・縮小)する速度が一定ではなく最初だけゆっくり(ease-in)や、最後だけゆっくり(ease-out)といった変化をつけることができます。
UnityのTween系でも登場しますね。
そちらでは覚えきれない位たくさんの種類のEaseTypeから目的に合致したものを利用することができます。
ではAngularではどうかというと、実は今回使っているアニメーション、というのはAngular独自のものではなくCSSのアニメーションです。
で、CSSで使用できるものはというと、下記のみです。
- ease(デフォルト)
- linear(はじめから終わりまで一定)
- ease-in(最初だけゆっくり)
- ease-out(最後だけゆっくり)
- ease-in-out(最初と最後だけゆっくり)
- cubic-bezier()(カスタム)
SCSSなどであればEasing Function 早見表で紹介されているようなEaseTypeが使えるようなのですが、
CSSでは種類がかなり少ない(またはカスタムで作成する)ようです。
カスタムについては次回辺りためしてみたいと思います。
アニメーションの開始・終了イベント
複数のアニメーションを連続で実行したい、アニメーションの実行後に何か処理をしたい、といった場合に、それを検知するイベントが欲しいところ。
そのような場合は、クリックイベントのように「start」「done」イベントを設定します。
Start
< button [@buttonState]="state" 〜省略〜(@buttonState.start)="onAnimeStarted()">Anime< /button>
Done
< button [@buttonState]="state" 〜省略〜(@buttonState.done)="onAnimeFinished()">Anime< /button>
アニメーションのトリガーである「buttonState」に紐付けられていますね。
あとはクリックイベントと同じく、gui-anime.component.tsに「onAnimeStarted()」や「onAnimeFinished()」を追加するだけです。