- 投稿日:2019-01-27T23:23:10+09:00
第3回「FirebaseAuthによるログイン・ログアウト導入」@FirebaseAuth+Nuxt.js+Go(v1.11)+GAE開発シリーズ
お題
前回でFirebaseAuth導入前の準備が出来たので、今回からようやくFirebaseAuthを導入する。
今回は、ログイン・ログアウト。ただし、メールアドレスとパスワードでログインするパターンのみ。前提
以下は他にいくらでもよい記事があるので省略。
- 開発環境の構築(GolangやらYarnやらのインストール等)
- Google Cloud Platform環境の取得(App Engine有効化)
- Firebase環境の取得
- Vue.jsやNuxt.jsのチュートリアル
開発環境
# OS
$ cat /etc/os-release NAME="Ubuntu" VERSION="18.04.1 LTS (Bionic Beaver)"# 依存モジュールのバージョン
package.jsonの内容より、以下の通り。"nuxt": "^2.3.4", "vuetify": "^1.3.14", "vuetify-loader": "^1.0.8", "@nuxtjs/axios": "^5.3.6"# Yarn
$ yarn -v 1.12.3# Golang
$ go version go version go1.11.4 linux/amd64実践
■FirebaseAuthをフロントエンドに導入
自作のWebサイトにFirebaseAuthの認証機能を使うにあたって、まず、Firebaseをフロントエンドのプロジェクトに導入する必要がある。
下記によると、FirebaseAPIを使うためのAPIキーやら認証ドメインやらの設定を書いてfirebase SDKのアプリ初期化コードを実行するとよいらしい。
https://firebase.google.com/docs/web/setup?hl=ja導入コマンドと結果確認
$ yarn add firebase 〜〜省略〜〜 $ yarn add firebase-admin 〜〜省略〜〜[package.json]$ cat package.json { "name": "frontend", 〜〜省略〜〜 "dependencies": { 〜〜省略〜〜 "firebase": "^5.8.0", "firebase-admin": "^6.5.0", 〜〜省略〜〜 }, 〜〜省略〜〜 }プラグインとして初期化コードを実装
Firebaseアプリの初期化は1回きりでいいのでプラグインとして実装
[frontend/plugins/firebase.js]import firebase from 'firebase' if (!firebase.apps.length) { firebase.initializeApp( { apiKey: process.env.apiKey, authDomain: process.env.authDomain, databaseURL: process.env.databaseURL, projectId: process.env.projectId, storageBucket: process.env.storageBucket, messagingSenderId: process.env.messagingSenderId } ); } export default firebaseAPIキー等の情報は
process.envから取得
実際の値は下記の環境に応じて適用されるファイルに記載(間違ってもpublicなGitHubリポジトリにアップしてはならない・・・)
※このあたり、ローカルでだけ動作確認するレベルならローカルストレージに実値を持たせておけばいいのだけど、GAEデプロイするとなるとそうもいかない。。。$ tree -L 1 frontend/ frontend/ ├── README.md ├── assets ├── components ├── dist ├── env.development.js ├── env.production.js ├── jest.config.js ├── layouts 〜〜省略〜〜[frontend/env.development.js]module.exports = { apiBaseUrl: 'http://localhost:8080/api/v1', apiKey: "<API_KEY>", authDomain: "<PROJECT_ID>.firebaseapp.com", databaseURL: "https://<DATABASE_NAME>.firebaseio.com", projectId: "<PROJECT_ID>", storageBucket: "<BUCKET>.appspot.com", messagingSenderId: "<SENDER_ID>", }[frontend/env.productio.js]module.exports = { apiBaseUrl: 'https://<PROJECT_ID>.appspot.com/api/v1', apiKey: "<API_KEY>", authDomain: "<PROJECT_ID>.firebaseapp.com", databaseURL: "https://<DATABASE_NAME>.firebaseio.com", projectId: "<PROJECT_ID>", storageBucket: "<BUCKET>.appspot.com", messagingSenderId: "<SENDER_ID>", }■メールアドレス・パスワードを用いたSignIn
FirebaseAuthコンソールで事前のユーザ作成
なにはともあれ、最初のユーザがいないとログインができないので作っておく。
loginコンポーネントにFirebaseAuthのSignIn機能を実装
frontend/components/login.vueにSignInロジックを追加[frontend/components/login.vue(<template>部分は省略)]<script> import firebase from '~/plugins/firebase' export default { data() { return { email: '', password: '', errMsg: '' } }, methods: { async login() { await firebase .auth() .signInWithEmailAndPassword(this.email, this.password) .then(res => { // ログイン正常終了時はログイン後の初期画面に遷移する。 this.$router.push('/') }) .catch(error => { this.errMsg = error.message console.log( 'errorCode:' + error.code + ', errorMessage:' + error.message ) }) } } } </script>動作確認
FirebaseAuthコンソールで事前登録したユーザのメールアドレスとパスワードを入力して「LOGIN」ボタン押下すると
ログイン後トップ画面に遷移する。
FirebaseAuthコンソールにて先ほど作成したユーザのログイン状況を見てみると
ちゃんとログイン日が記載されている。
試しにメールアドレスとパスワードなしでログインしてみると
ちゃんとFirebaseAuthのSDKからエラーメッセージが取得できる。
■SignOut
SignInが終わったら、今度は当然SignOut
defaultレイアウトにFirebaseAuthのSignOut機能を実装
frontend/layouts/default.vueにSignOutロジックを追加[frontend/layouts/default.vue]<template> <v-app light> <v-btn class="mx-1 my-2 px-3 py-2 lime" @click="logout" > LOGOUT </v-btn> <nuxt /> </v-app> </template> <script> import firebase from '~/plugins/firebase' export default { methods: { async logout() { await firebase .auth() .signOut() .then(res => { // ログアウト正常終了時はログイン画面に遷移する。 this.$router.push('/login') }) .catch(error => { console.log( 'errorCode:' + error.code + ', errorMessage:' + error.message ) }) } } } </script>動作確認
「LOGOUT」ボタンを押下すると
SignOutが正常終了してログイン画面に遷移する。
■ここまでの全ソース
- 投稿日:2019-01-27T23:22:21+09:00
第2回「FirebaseAuth導入前(ログインフォーム実装とバックエンドプロジェクトのガワ作成)」@FirebaseAuth+Nuxt.js+Go(v1.11)+GAE開発シリーズ
お題
前回に続き、今回もまだFirebaseAuth導入前。ログインフォームを実装する。
また、フロントだけでなくバックエンドともゆくゆく連携が必要なのでガワを作っておく。前提
以下は他にいくらでもよい記事があるので省略。
- 開発環境の構築(GolangやらYarnやらのインストール等)
- Google Cloud Platform環境の取得(App Engine有効化)
- Firebase環境の取得
- Vue.jsやNuxt.jsのチュートリアル
開発環境
# OS
$ cat /etc/os-release NAME="Ubuntu" VERSION="18.04.1 LTS (Bionic Beaver)"# 依存モジュールのバージョン
package.jsonの内容より、以下の通り。"nuxt": "^2.3.4", "vuetify": "^1.3.14", "vuetify-loader": "^1.0.8", "@nuxtjs/axios": "^5.3.6"# Yarn
$ yarn -v 1.12.3# Golang
$ go version go version go1.11.4 linux/amd64実践
frontend/components/login.vueを下記の通り修正してみる。
せっかくvuetifyを使っていても、デザインは適当。。。
とりあえず、FirebaseAuthを使うにあたって、べたな「メールアドレス」と「パスワード」での認証を試す予定なので、フォームの要素もそれに合わせる。
※バリデーションやエラーハンドリングなど、お試しレベルでは不要なものは未実装。修正後のソース
[frontend/components/login.vue]<template> <v-layout class="py-3" > <v-form> <v-text-field v-model="email" label="Email" class="py-2" /> <v-text-field v-model="password" label="Password" :type="`password`" class="py-2" /> <v-btn class="mx-1 my-2 px-3 py-2 lime" @click="login" > LOGIN </v-btn> </v-form> </v-layout> </template> <script> export default { data() { return { email: '', password: '' } }, methods: { login: function() { console.log('login') // FIXME: ここにFirebaseAuthを用いたログイン処理を書く想定 // ログイン正常終了時はログイン後の初期画面に遷移する。 this.$router.push('/') } } } </script>修正後の画面表示
■ログイン後トップ画面のデザイン修正
固定文字列を表示していた
frontend/pages/index.vueを修正する。
ログイン後には、お知らせくらい表示するだろうということで、「お知らせコンポーネント」を追加。
それを使うことにする。修正後のソース
[frontend/pages/index.vue]<template> <v-content> <notice /> </v-content> </template> <script> import notice from '~/components/notice.vue' export default { components: { notice } } </script>[frontend/components/notice.vue]<template> <v-layout class="py-3" > <v-list two-line> <v-list-tile v-for="notice in notices" :key="notice"> <v-list-tile-content class="mb-2"> <v-list-tile-title>{{ notice.sentence }}</v-list-tile-title> </v-list-tile-content> </v-list-tile> </v-list> </v-layout> </template> <script> export default { data() { return { notices: [] } }, mounted() { this.$axios .get(process.env.apiBaseUrl + '/notices') .then(res => { console.log(res.data) this.notices = res.data }) .catch(err => { console.log(err) }) } } </script>お知らせの内容は axiosを用いてバックエンド(GoでWebAPIを実装)から取得する。
修正後の画面表示
■バックエンド(GoによるWebAPI実装)のディレクトリ構造
巷ではクリーン・アーキテクチャが流行りだけど、お試しプロジェクトでそこまでの汎用性は不要なので、当初は
main.goだけで済まそうとした。
ただ、一応、ある程度機能拡張する可能性も考慮し、MVC+S くらいの作りにはしておくことに。
※MVC+Sに関しては過去に記事化していた。
https://qiita.com/sky0621/items/c7b196a1ba0e126cc3f5$ tree -L 2 backend/ backend/ ├── README.md ├── app.yaml ├── controller │ ├── apierror.go │ ├── form │ ├── notice.go │ ├── response │ └── router.go ├── go.mod ├── go.sum ├── index.yaml ├── logger │ └── logger.go ├── main.go ├── middleware │ ├── basic.go │ └── custom.go ├── model │ ├── dto.go │ └── notice.go ├── service │ └── notice.go ├── system │ ├── local.go │ └── setting.go ├── util │ ├── stringbuilder.go │ └── util.go └── view★controller -> service -> model の呼び出し関係
★viewの下にはfrontendでビルドしたファイルを出力する。■バックエンドの使用フレームワーク
■バックエンドのつくり
この時点ではFirebaseAuthとの絡みは出てこないので省略。
実際のソースは下記。
https://github.com/sky0621/Dotato-di-una-libreria/tree/38261c1768ca8da0aba77ced9d5a1b795172e89c/backend■ここまでの全ソース
https://github.com/sky0621/Dotato-di-una-libreria/tree/38261c1768ca8da0aba77ced9d5a1b795172e89c
- 投稿日:2019-01-27T16:37:13+09:00
Golangのエラーハンドリングの基本
想定している読者
Goのエラーハンドリングについて体系だった記事が見つからなかったので、色々調べたことを整理して備忘録も兼ねて記事にしました。
以下のような方を読者対象に記事を書いています。
- Goのエラーハンドリングの基本的なやり方を知りたい
- スタックトレースを標準エラー出力したい
- エラーの種類に応じてステータスコードを変えたい
まずデファクトになっているエラーパッケージpkg/errorsの使い方を確認して、実際のWebアプリケーションで追加で実装しないといけないことを説明していきます。
そもそもerrorとは?
goのエラーは以下のようなエラーメッセージを返す関数を実装していれば満たせるインターフェースです。
// cf. https://golang.org/pkg/builtin/#error type error interface { Error() string }実際には返されたエラーがnilかどうかで条件分岐して、nilでない場合は
error.Error()でエラー内容を出力するような使われ方をします。
例として、引数で与えられたファイルを開いて、それをJSONとして扱って、map[string]interface{}型の値(jsonMap)に変換してみます。import ( "encoding/json" "fmt" "io/ioutil" ) func main() { // 中身が空のファイルをJSONとして扱うとjson.Unmarshalでエラーになる if _, err := unmarshalToMap("src.json"); err != nil { // err.Error()の結果が出力される fmt.Println(err) // unexpected end of JSON input } } func unmarshalToMap(src string) (map[string]interface{}, error) { jsonMap := map[string]interface{}{} // ファイルパスからファイルを読み込む data, err := ioutil.ReadFile(src) if err != nil { return jsonMap, err } // ファイルの内容をJSONとみなして、key, valueの対応をmap[string]interface{}型の値にする if err := json.Unmarshal(data, &jsonMap); err != nil { return nil, err } return jsonMap, nil }しかし、引数で渡されたファイル
src.jsonは何も書かれていないので(JSONの書式に沿っていないので)エラーが返ってきます。$ go run main.go unexpected end of JSON inputこのように、
Error() stringさえ実装してエラーメッセージを返せればerror型を満たすことができます。一見、上記のような方法でも問題がなさそうですが、実際のアプリケーションは実装が多くなり、複雑になりがちにも関わらず、
unexpected end of JSON inputというエラー内容だけだと、どのファイルの内容をjson.Umarshalした時に起こったのか分からないとデバッグが困難になります。そこで重要なのが、返ってきたエラーに「何を」、「どこで」、「どんな処理で」起こったのかコンテキスト情報を付与することです。コンテキスト情報をエラー内容に含める
返ってきたエラーにコンテキスト情報を付与する一番簡単な方法は
fmt.Errorを利用することです。
これは、指定したフォーマットにしたがって、第二引数以降をフォーマットし、新しいエラーメッセージを持つエラーを作って返すことができます。先ほどの実装で
fmt.Errorfを使ってみます。func unmarshalToMap(src string) (map[string]interface{}, error) { jsonMap := map[string]interface{}{} // ファイルパスからファイルを読み込む data, err := ioutil.ReadFile(src) if err != nil { return jsonMap, err } if err := json.Unmarshal(data, &jsonMap); err != nil { return nil, fmt.Errorf("read %s, %s", src, err) // ここをfmt.Errorfに置き換えた } return jsonMap, nil }これによって以下のようにどのファイルを読み込んでエラーがでたのかメッセージに含めることができます。
$ go run main.go read src.json, unexpected end of JSON inputこのように
fmt.Errorfを使うことで「どこで」、「どんな処理で」エラーが起こったのか知ることができます。fmt.Errorfの問題点
しかし、これで問題なしかというとそうではありません。理由は
fmt.Errorfは元のerrorインターフェースを実装するある型と値を消失させるからです。
fmt.Errorfの実装を見ると、内部でerrors.Newを呼び出していることがわかります。// Errorf formats according to a format specifier and returns the string // as a value that satisfies error. func Errorf(format string, a ...interface{}) error { return errors.New(Sprintf(format, a...)) }
errors.Newとは、errorインターフェースを実装したエラーメッセージだけを持つ構造体を返します。// cf. https://golang.org/pkg/errors/#New // New returns an error that formats as the given text. func New(text string) error { return &errorString{text} } // errorString is a trivial implementation of error. type errorString struct { s string } func (e *errorString) Error() string { return e.s }しかし、ライブラリによって、独自の
errorインターフェースを実装した構造体を定義して、エラーメッセージ以外の情報を付与していることがよくあります。これによって、受け取った(errorインターフェース型に抽象化された)エラーを型アサーションして元の型に戻して、付加された値を取り出すことができます。fmt.Errorfは上記のerrorString構造体に作り直してしまうのでこれをできなくしてしまいます。例えば、JSONのKey, Valueの対応はSliceで表現することはできないのでUnmarshalするとエラーになりますが、そのエラーはJSONのバイト列におけるエラーが起こった位置やUnmarshalしようとした型の名前をエラーメッセージ以外を持っています。
具体的には、以下のUnmarshalTypeErrorがerrorインターフェースを実装しており、それが返ってきます。
// cf. https://golang.org/pkg/encoding/json/#UnmarshalTypeError // An UnmarshalTypeError describes a JSON value that was // not appropriate for a value of a specific Go type. type UnmarshalTypeError struct { Value string // description of JSON value - "bool", "array", "number -5" Type reflect.Type // type of Go value it could not be assigned to Offset int64 // error occurred after reading Offset bytes Struct string // name of the struct type containing the field Field string // name of the field holding the Go value } func (e *UnmarshalTypeError) Error() string { if e.Struct != "" || e.Field != "" { return "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String() } return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() }実際に、対応していない型にUnmarshalするとUnmarshalTypeError型に型アサーションして、エラーメッセージ以外を取り出してみます。
import ( "encoding/json" "fmt" ) var jsonData = []byte(` { "name": "user", "password": "pass" } `) func main() { // []string型はJSONのKey, Valueの内容を持つことができないのでUnmarshalするとエラーになる valueOfInvalidType := make([]string, 0) err := json.Unmarshal(jsonData, &valueOfInvalidType) switch err := err.(type) { case *json.UnmarshalTypeError: fmt.Printf("type: %s\n", err.Type) fmt.Printf("offet: %d\n", err.Offset) fmt.Printf("Error(): %s\n", err) default: fmt.Println(err) } }$ go run main.go type: []string offet: 2 Error(): json: cannot unmarshal object into Go value of type []stringこのようにしたくとも、
fmt.Errorfが既存のError()の結果以外の情報を切り捨ててしまうので、以下のような問題に直面します。
- errorインターフェースの元の型に応じて条件分岐ができなくなる
- errorインターフェースの元の型の持つコンテキスト情報が消失する
これらの問題を解決する手段でかつ、現在(2019/01/22)でデファクトとなっているエラーパッケージとして、
pkg/errorsがあります。pkg/errorsを使う
では、具体的に
pkg/errorsで上記問題を解決できるのかというと、以下のWrapとCauseを用います。func Wrap(err error, message string) errorfunc Cause(err error) errorまず、
Wrapを使うことで元のerrorインターフェースを実装した型と値を保持して、エラーメッセージだけコンテキスト情報を追加した新しいものにできます。if err := json.Unmarshal(data, &jsonMap); err != nil { // failed to unmarshal src.json: unexpected end of JSON input return nil, errors.Wrap(err, "failed to unmarshal src.json") }この結果だけ見れば、
fmt.Errorf("failed to unmarshal scr.json: %s", err)した結果と同じですが、%+vでフォーマットするとStackTraceを出力することもできます。// cf. https://godoc.org/github.com/pkg/errors#hdr-Formatted_printing_of_errors if err := json.Unmarshal(data, &jsonMap); err != nil { fmt.Printf("%+v", err) }main.unmarshalToMap /go/src/github.com/shoichiimamura/error-handling-example/main.go:30 main.main /go/src/github.com/shoichiimamura/error-handling-example/main.go:18 runtime.main /usr/local/go/src/runtime/proc.go:198 runtime.goexitさらに
Causeを使うことでWrapされたエラーから元のerrorインターフェースを実装した型と値を取り出すことができます。
より厳密に言うと、causerインターフェースを実装していない一番最後のerrorインターフェース型を取り出します。// cf. https://github.com/pkg/errors/blob/master/errors.go#L269 func Cause(err error) error { type causer interface { Cause() error } for err != nil { cause, ok := err.(causer) if !ok { break } err = cause.Cause() } return err }これを使うと、以下のように元のエラーの型に応じた条件分岐が可能になります。
switch err := errors.Cause(err).(type) { case *json.UnmarshalTypeError: fmt.Println(err.Offset) case *json.InvalidUnmarshalError: fmt.Println(err.Type) default: fmt.Println(err) }この
Causeでエラーの型に応じた条件分岐ができることがわかりました。しかし、実際のAPIを持つアプリケーションサーバーの開発では、エラーの種類に応じてHTTPステータスコードを変えたいことがよくあります。これを実現するためには、
pkg/errorsを拡張したエラーパッケージを作る必要があります。次はpkg/errorsを利用して、エラーの種類の応じたHTTPステータスコードを返す実装をしてみます。エラーに応じてステータスコードを選ぶ
pkg/errorsを使った以下のようなerrorインターフェースを実装した構造体を作ることで、WrapやCauseを使えつつ、エラータイプを取得できるようになります。errors.Wrapfなどは説明を簡単にするためにラップしていませんが、同じような要領で実装することもできます。import ( "github.com/pkg/errors" ) // ErrorType エラーの種類 type ErrorType uint const ( Unknown ErrorType = iota InvalidArgument Unauthorized ConnectionFailed ) // ErrorTypeを返すインターフェース type typeGetter interface { Type() ErrorType } // ErrorTypeを持つ構造体 type customError struct { errorType ErrorType originalError error } // New 指定したErrorTypeを持つcustomErrorを返す func (et ErrorType) New(message string) error { return customError{errorType: et, originalError: errors.New(message)} } // Wrap 指定したErrorTypeと与えられたメッセージを持つcustomErrorにWrapする func (et ErrorType) Wrap(err error, message string) error { return customError{errorType: et, originalError: errors.Wrap(err, message)} } // Error errorインターフェースを実装する func (e customError) Error() string { return e.originalError.Error() } // Type typeGetterインターフェースを実装する func (e customError) Type() ErrorType { return e.errorType } // Wrap 受け取ったerrorがErrorTypeを持つ場合はそれを引き継いで与えられたエラーメッセージを持つcustomErrorにWrapする func Wrap(err error, message string) error { we := errors.Wrap(err, message) if ce, ok := err.(typeGetter); ok { return customError{errorType: ce.Type(), originalError: we} } return customError{errorType: Unknown, originalError: we} } // Cause errors.CauseのWrapper func Cause(err error) error { return errors.Cause(err) } // GetType ErrorTypeを持つ場合はそれを返し、無ければUnknownを返す func GetType(err error) ErrorType { for { if e, ok := err.(typeGetter); ok { return e.Type() } break } return Unknown }これによって、任意のErrorTypeを持つエラーを作って、Controller層でそれを取り出し、対応するステータスコードを選ぶことができます。
func main() { err := Unauthorized.New("ある認証の処理内で返されたエラー") fmt.Println(statusCode(err)) // 401 } func statusCode(err error) int { switch GetType(err) { case ConnectionFailed: return http.StatusInternalServerError // 500 case Unauthorized: return http.StatusUnauthorized // 401 default: return http.StatusBadRequest // 400 } }Panicでアプリケーションをクラッシュさせない
panicとは関数呼び出し元の処理を連続的に中断するgoの組み込み関数のことです。
明示的に呼び出すこともできますし、他にはnilポインタにメソッド呼び出しをした時などでも起きます。type User struct { Name string } func (u *User) Name() string { return u.Name } var user *models.User user.Name() // panicpanicが起こるとdefer内でrecover(後述)しないと、プログラムはCrashしてしまうので、そうさせないようにPanicが起きた旨をエラーにして返してあげるようにします。そのためには、まず、deferとrecoverについて概要を押さえる必要があります。
deferは、関数を登録することができ、その定義元の関数がreturnされた後に呼び出され、panicが起こった場合も呼びされます。
そのため以下の実装は、returnされた値(i)をdeferでインクリメントして出力しているので、結果は2になります。func a() (i int) { defer func() { i++ fmt.Printf("%d\n", i) }() return 1 }また、deferに登録した関数は後入れ先出し(Last in First out)で呼び出されるので、以下の実装は
3210を出力します。func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } }そして、recoverはpanicが起こったgoroutineを再び制御する組み込み関数で、panicが起こった後にdeferが呼ばれ、その中でpanicの伝播を止める役割を担います。defer外でrecoverしてもpanic時には呼び出されず、nilを返すだけなので、この使われ方以外はなさそうです。
以下は、panicが起こった時にdefer内でrecoverを呼び出し、panicの伝播を止めてエラーを返しています。
import ( "fmt" "github.com/pkg/errors" ) func panicAndRecover() (err error) { defer func() { if r := recover(); r != nil { err = errors.New(fmt.Sprintf("recovered: %v\n", r)) } }() panic("panic at panicAndRecover") return } func main() { err := panicAndRecover() fmt.Println(err) // recovered: panic at panicAndRecover }こうすることで、panicが起こってもエラーとして扱うことができます。
参考
- 投稿日:2019-01-27T15:37:12+09:00
daemontools: setlock by katoris2014
daemontools
http://keisyu.hateblo.jp/entry/2014/02/12/234834
setlock
https://qiita.com/mogulla3/items/0a955196c524712f48ba
go-setlock
- 投稿日:2019-01-27T14:06:07+09:00
Vue.js + TypeScript + GAE/Go で SPA 開発をしている感想
自己紹介
Twitter @mochizukikotaro
普段の仕事は、
- Rails、CakePHP がメイン
- Laravel はこれから入門
- Vue.js、AWS、GCP、k8s、Go は少し触ります
プライベートで、
- SPA の Webアプリケーションを作り中
- 英語勉強中。2019年中に TOEIC 900点をとりたい(615点/2018年)
構成
今
Vue.js + TypeScript + Go + GAE/Go 1.111年前
Vue.js + Vuex + Go + heroku
変更の理由
- わりと勉強がてらのプロジェクトなので、深い理由はなく。
- TS はみんなが良いっていうから。
- GAE/Go は Go の runtime を動かすだけなら heroku よりカンタンなイメージだから。
- Vuex は前しんどくて、今はつくり直し中でそこまで大きくないから。なので、今後導入する可能性は高い。
Vue.js + TypeScirpt + Vuex 使っている方いたら、感触教えてください
![]()
良い点、学んだ点
- TS 気持ちいいい
- GAE/Go へのデプロイが楽
- 開発環境の CORS は vue の proxy で対応
困った点
- TS の declaration file がない問題
- GAE/Go のスタンダード環境だとインスタンスに ssh できない(そんな困ってない)
- GAE/Go へのデプロイ
gcloud app deployが遅い
良い点、学んだ点
TS 気持ちいい
あんまり頑張ってないです。使えるところは使う感じです。
例えば interface 使うので、オブジェクトがどんなものか分かるし、エディタの補完も気持ちいい。IUser.vue<script lang="ts"> export interface IUser { ID: number; ScreenName: string; TwitterID: number; ProfileImage: string; } </script>interface に対して
Iprefix を使う使わない問題みたいのもあるようですが、ぼくは結論Iをつけています。
GAE/Go へのデプロイが楽
app.yamlruntime: go111$ gcloud app deployこれだけで、GAE にデプロイができる。
開発環境の CORS について
開発環境は docker-compose でやっていますので、node(frontend) と go(backend) のコンテナがあります。
docker-compose.ymlversion: '3' services: golang: ports: - "8081:8080" ... note: ports: - "8080:8080" ...node は
loalhost:8080で server はlocalhost:8081で動かしているので、フロントからサーバーへのリクエストで CORS 問題がおきます。
以前はサーバー側でなんとかしてました ?
server.goe.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"http://localhost:8080"}, AllowCredentials: true, }))
今は vue の proxy で対応 ?
vue.config.jsmodule.exports = { devServer: { proxy: { '/api': { target: 'http://golang:8080' } } } }これで、 AllowOrigins とか書かなくて済みました。
Vue CLI 3 Configuration Reference: devServer.proxy
困った点
TS declaration file がない問題
なにかしらのパッケージを使っている状態で
yarn buildなどすると現れるエラー。import Prism from "vue-prism-component";$ yarn build ... Could not find a declaration file for module '...'.
PullRequest を見てみると
ちゃんとでています。が、マージはされていません。
マージされるのを祈るばかり...?
結局どうしたのか
今回使いたかったのは、 vue-prism-component というコードハイライターの Prismjs を vue で使いやすくするパッケージでした。
なので、vue-prism-component を使わずに直接 Prismjs を使ってごにょごにょしようとおもったのですが...。
なんかうまく動かなかったのでフロントでやるのをやめて、サーバーサイドで対応することにしました ?
chroma: A general purpose syntax highlighter in pure Go
結果、フロントのコンポーネント構成などがスッキリしてよかったです...。
GAE/Go 1.11 の困りごと
- スタンダード環境だと ssh できないけどフレキシブル環境だといけそう(ためしてません)
- gcloud app deploy は普通に遅いと思いますが、
.gcloudignoreで適切に ignore しておかないと、本当に無駄な時間をすごくことになるとおもいます。(泣きそうでした ?.gcloudignorefrontend/node_modules/ frontend/public/ frontend/src/ vendor/
以上ありがとうございました!
補足(全体のファイル構成)
$ tree . -L 2 . ├── Gopkg.lock ├── Gopkg.toml ├── README.md ├── app.yaml ├── containers │ ├── golang │ └── node ├── db │ └── db.go ├── docker-compose.yml ├── frontend │ ├── README.md │ ├── babel.config.js │ ├── dist │ ├── node_modules │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── src │ ├── tsconfig.json │ ├── vue.config.js │ ├── yarn-error.log │ └── yarn.lock ├── handler │ ├── about.go │ ├── auth.go │ ├── note.go │ └── user.go ├── highlight │ ├── transform.go │ ├── transformNote.go │ ├── transformNote_test.go │ └── transform_test.go ├── migrations │ ├── 1812151700_add_users_table.down.sql │ ├── 1812151700_add_users_table.up.sql │ ├── 1812160000_add_notes_table.down.sql │ └── 1812160000_add_notes_table.up.sql ├── model │ ├── note.go │ └── user.go ├── repository │ └── note.go ├── server.go ├── templates │ └── index.html └── vendor ├── github.com └── google.golang.org
- 投稿日:2019-01-27T05:54:57+09:00
goとUNIXドメインソケットでpub/subっぽいことができるコマンドをつくる
UNIXのパイプの延長で、
- 書き込み側プロセスはひとつ
- 読み込み側プロセスは複数
- 書き込み側プロセスで行を書き込むと、読み込み側プロセス全て書き込んだ行が出力される
- 読み込み側プロセスは任意のタイミングで読み込みを開始できる(読み込みを開始した以降に書き込みされた行を読み込む)
という挙動のものがほしい。
(pub/subとはちょっと違うかもしれない)これは
tail -f -n 0で達成できますが、何らかの方法でファイルをときどき消すなどしてやらないとファイルが無限にでかくなっていきます。UNIXドメインソケットを使うと良いかもと思ったのですが、UNIXドメインソケットを読み書きできるいい感じのコマンドがなかったのでgoで実装してみました。
コード
pub
pub.gopackage main import ( "bufio" "io" "log" "net" "os" ) var connections = make(map[net.Conn]bool) func main() { go acceptConnections() stdin := bufio.NewReader(os.Stdin) for { b, err := stdin.ReadByte() if err == io.EOF { break } for conn := range connections { _, err := conn.Write([]byte{b}) if err != nil { log.Println("socket write error:", err) conn.Close() delete(connections, conn) log.Println("connections count: ", len(connections)) } } //log.Println("wrote:", b) } } func acceptConnections() { listener, err := net.Listen("unix", "./sock") if err != nil { log.Println("Listen error: ", err) return } defer listener.Close() for { conn, err := listener.Accept() if err != nil { log.Println("Accept error: ", err) continue } log.Println("Accepted") connections[conn] = true log.Println("connections count: ", len(connections)) } }sub
sub.gopackage main import ( "fmt" "net" ) func main() { conn, err := net.Dial("unix", "./sock") if err != nil { fmt.Printf("Dial error: %s\n", err) return } defer conn.Close() buf := make([]byte, 1024) for { n, err := conn.Read(buf) if n == 0 { break } if err != nil { fmt.Printf("Read error: %s\n", err) } fmt.Print(string(buf[:n])) } }使ってみる
ビルドします。
ビルド$ go build pub.go $ go build sub.go書き込み側プロセスを起動します。
1秒ごとに時刻を出力するワンライナーの出力をパイプでつないでみます。送信側$ while :; do date; sleep 1; done | ./pub読み込み側のプロセスを起動すると、
dateの結果がずらずらと流れてきます。受信側$ ./sub 2019年 1月27日 日曜日 05時47分16秒 JST 2019年 1月27日 日曜日 05時47分17秒 JST 2019年 1月27日 日曜日 05時47分19秒 JST 2019年 1月27日 日曜日 05時47分20秒 JST別のターミナルを起動してもうひとつ
./subをたちあげると、起動したタイミング以降に書き込まれたdateの結果がずらずらと流れてきます。ToDo
- 終了処理をちゃんとやる
- UNIXドメインソケットのパスを引数で指定できるようにする
- 入出力が逆バージョンもつくったら便利なのでは、複数のセンサの値を読んでひとつのログに書き出したりとかに使えそう
- コマンド名はやはりpub/subではない気がするし、なんか考える
- 投稿日:2019-01-27T00:35:23+09:00
AWS Lambda Goで画像アップローダを作る
AWS Serverless Uploader
https://aws-serverless-uploader.netlify.com/フロントエンド
- React
- Netlify
ReactプロジェクトをNetlifyにデプロイ。
CSSフレームワークはBulma。
API通信はAxiosを使用。バックエンド
- AWS Lambda Go
- API Gateway
- S3
- DynamoDB
- SAM CLI
Serverless frameworkかSAM CLIかは好きな方で良さそう。
自分的にはAWS謹製という点に惹かれてSAM CLIを選定。sam init --runtime goでプロジェクトの雛形をバッと作ってくれてすぐにデプロイできるのと、AWS上の構成をコード管理できる(=infrastructure as code)ので重宝した。
GoでS3
aws-sdk-goを使用。
package main import ( "bytes" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" ) func s3Put(img []byte) error { svc := s3.New(session.New(), &aws.Config{ Region: aws.String("ap-northeast-1"), }) if _, err := svc.PutObject(&s3.PutObjectInput{ Bucket: aws.String("バケット名"), ACL: aws.String("private"), ServerSideEncryption: aws.String("AES256"), Key: aws.String("キー名"), Body: bytes.NewReader(img), }); err != nil { return err } return nil }GoでDynamoDB
素のaws-sdk-goだけの実装はダルいようなのでgregu/dynamoライブラリを使用。
package main import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/guregu/dynamo" ) type Image struct { Name string `json:"name" dynamo:"Name"` URL string `json:"url" dynamo:"Url"` CreatedAt string `json:"createdAt" dynamo:"CreatedAt"` } // 書き込み func dynamoPut(data Image) error { db := dynamo.New(session.New(), &aws.Config{Region: aws.String("ap-northeast-1")}) if err := db.Table("テーブル名").Put(data).Run(); err != nil { return err } return nil } // 全件取得 func dynamoGetAll(data *[]Image) error { db := dynamo.New(session.New(), &aws.Config{Region: aws.String("ap-northeast-1")}) if err := db.Table("テーブル名").Scan().All(data); err != nil { return err } return nil }次はAPIGateway+Lambda(Go)+Cognitoで認証処理を検証する予定。













