- 投稿日:2020-07-26T21:33:20+09:00
Vue + Vue Router + Vuex + Laravel のチュートリアル(@MasahiroHarada 様 作成)を進める最中に詰まった部分の原因と対策
はじめに
こちらの記事は、@MasahiroHarada 様が作成された以下の記事をのチュートリアルを、実際に自分が行ってみて、詰まった部分や勘違いしていた部分を、他のチュートリアルに挑戦される方向けに残したものです。
Laravel 6 & PHP 7.4 対応】Vue + Vue Router + Vuex + Laravel チュートリアル(全16回)を書きました。
本当に素晴らしい記事で、Laravel、Vue、spa設計の知見が深まりました。
この場を借りて、改めて感謝申し上げます。※記事の執筆にあたり、一応ご本人から確認はいただいておりますが、共著でもないため、間違いやご指摘、ご意見は全て私の方に直接いただきますよう、お願い申し上げます。
それでは、よろしくお願いします。
私の開発環境と前提
私が使用した開発環境です。
Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (3) SPA開発環境とVue Routerの記事を参考に、Dockerで作成しました。
Laravelは7.19.0、OSはMacです。
詳しいことは上記の記事にまとめてくださっていますが、前提条件としてご了承ください。
次章より、実際に詰まった部分と解決策を述べます。npm run watch で何も表示されない
最初に遭遇したエラーは、(3) SPA開発環境とVue Routerのフロントエンドのビルドの項目で、
npm run watch
を入力しても何も表示されなかったことです。これは、SPAの仕組み上当然のことで、バックエンドの(Laravelの)サーバーを立てていなかったことが原因でした。
ターミナル等でシェルをもう一つ用意し、バックエンドのサーバーを
php artisan serve --host 0.0.0.0 --port 8081
で立ててから、もう一つの方でnpm run watch
を行うと無事表示されました。Illuminate\Foundation\Auth\RegistersUsers.php がない
(4) 認証APIの部分です。
これは、私と同じLaravelの7系、もしくは6系を使っている方は遭遇するかと思います。対策の詳細はこちらの公式にも載っています。
https://readouble.com/laravel/7.x/ja/authentication.html具体的には、以下のコードをcomposerファイルのあるディレクトリで実行すると現れてくれるはずです。
# Laravel6系 composer require laravel/ui:^1.0 --dev # Laravel7系 composer require laravel/ui参考記事
更新! Laravel6/7「make:Auth」が無くなった 〜Laravel6/7でのLogin機能の実装方法〜MyMemoなぜか新規登録ができない
これが個人的には一番びっくりなエラーでした。コードはあっていて新規登録をしようとしても、
POST http://localhost:3000/api/register 405 (Method Not Allowed) message: "The POST method is not supported for this route. Supported methods: GET, HEAD."
というエラーが出ます。これの解決策は、なんとパスワードを8文字以上にすることでした。
Railsのdeviseというログイン機能を作ってくれるライブラリをよく使用していて、そちらは6文字だったので油断しておりました。参考記事
ユーザー認証のパスワード制限の変更コントローラーで処理がされない
チュートリアルを進めていき、理解している部分は基本的にコピペで行っていたのですが、それ通りに貼り付けているはずなのにコントローラーが反応してくれない場面がありました。
原因はDIのし忘れ(namespaceの書き忘れ)でした。
Laravelでは別ファイルから使いたい要素を持ってくるときに、
use App\Http\Requests\StoreComment;
と言った形で主にファイル冒頭に記載をするのですが、内容のコードだけを貼って、こちらをコピペすることを忘れていました。結果、参照ができずにエラーが出たわけです。しっかりと記事を読んで、コピーのし忘れがないか確認しましょう。
Failed to load resource: the server responded with a status of 405 (Method Not Allowed) が出る
これは、ルーティングに誤りがありました。今回のチュートリアルでは、api.phpとweb.php二つのルーティングがあります。apiを叩く処理を間違った方に書いており、このエラーが発生しました。
「ルーティングを書いている場所はあっているか?」チェックをしてみると解決するかもしれません。
エラーメッセージがインポートできない
(8) エラーハンドリングの章で記載ミス?と思われるものがあります。(2020/7/26現在)コメント欄で@MasahiroHarada 様が回答されていますが、一応記事がそのままだったのでこちらにも記載いたしました。
router.jsimport SystemError from './pages/errors/System.vue' // ここのままだとエラーになるので、 // import SystemError from './pages/errors/500.vue' // としてあげましょう /* 中略 */ const routes = [ /* 中略 */ { path: '/500', component: SystemError } ]写真投稿APIのテストが通らない
Dockerで作開発をしていると、 (9) 写真投稿APIのテストが通りません、原因は記事のコメント欄にもあったのですが、一応こちらにまとめます。
基本的には紹介されているこちらの記事通りにやっていただきたいのですが、私はDocker初学者だったので「再ビルドしてください」というのが、どうするのかわかりませんでした。
私の場合
docker build -t vuesplash_vuesplash_web .
で解決しました。(ピリオドも必要です)謝辞
以上が、自分がチュートリアル実行時に遭遇したエラーです。初歩的なミスもあり、そのような振り返り学習ができたという点でも、こちらのチュートリアルは素晴らしかったです…本当にありがとうございました。
- 投稿日:2020-07-26T20:07:02+09:00
VueのComposition APIをTypescriptで使う際に覚えておくと便利なこと
はじめに
Composition API を Typescript で使う際にハマったことの備忘録です。
随時更新していく予定。Vue3 の環境構築はこちらから。vue-cli からかんたんに Vue3 環境を作成できます。
Version
Vue: 3.0.0 @vue/cli: 4.5.0 Typescript: 3.9.3Tips
1. Recursive Type References
Composition API RFC のコードをそのまま Typescript で実行すると、以下のエラーが表示されます。
state
の定義中に、 自分自身(state
)を使用しているためです。
(Typescript のバージョンによっては発生しないらしいです)'state' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.setup() { const state = reactive({ count: 0, double: computed(() => state.count * 2) }); return { state }; }// use interface interface Counter { count: number; double: number; } setup() { const state: Counter = reactive({ count: 0, double: computed(() => state.count * 2) }); return { state }; }// use ref setup() { const count = ref(0); const double = computed(() => count.value * 2); function increment() { count.value++; } return { count, double, increment }; }2.
props
で型推論が効くようにするそのままだと
props
にほとんど補完が効かないため、props
にComponentObjectPropsOptions
を付与します。
こうすることで、props
に指定したprop
に対し、default
,required
といった property が補完されます。import { defineComponent, ComponentObjectPropsOptions } from "vue"; interface CustomProps { msg: string; } export default defineComponent({ name: "Home", props: { msg: { required: true, type: String, default: "", }, } as ComponentObjectPropsOptions<CustomProps>, });以下の図で、
required
が推論されているのが分かります。また、
defineComponent
の generics に任意の Type を与えることで、prop
の型を縛ることができます。
上記のコードでは、CustomProps.msg
の型がstring
のため、props.msg.type
にString
以外を指定するとエラーになります。
※defineComponent<CustomProps>
でもできそうですが、うまくいきませんでした...
うまい方法を知っている方がいらっしゃったらご教示いただけると嬉しいです。
- 投稿日:2020-07-26T16:44:41+09:00
Vue.jsとGo言語で簡単なWebアプリを作成する(Dockerで開発環境構築�)
初投稿です。
過去の経験の復習かねて、書き起こしました。概要
成果物はこちらになりますので、VueやGoを始めてみたい方への力になれれば幸いです。
https://github.com/aocm/vue-go-spa-sample方針としては、なるべくローカルを汚さないことを最優先に考えました。
開発者ごとに環境が違うのも大変ですし、他のプロジェクト(自分の個人開発・仕事)で他のバージョンを利用していたら思わぬ影響が出てしまうのも嫌ですので、直接ローカルにインストールするのはなるべく避けたいです。今回はDockerを使って、コンテナ内部で開発環境を作って作業していきます。
ただ、Gitについてはローカル(ホスト)でしか利用できない環境になってしまったので、そこは課題です。
(最低限、更新差分はコンテナの中でも確認できるようにしたほうが親切でした。)対象読者
- Dockerは最低限環境構築できているけど、Webアプリ開発ってどこから手を付ければいいんだ?という人
- Dockerで開発環境を構築するサンプルが見たい人
- Goの簡単な動くサンプルが見たい人
- Vueの簡単な動くサンプルが見たい人
※きれいなコードを書くことが目的ではないのでご了承ください
書くこと
- なるべくローカルを汚さないような開発環境構築の方法
- ローカルを汚す...ここでは、各プラグインやソフトウェアをローカルにインストールしまくる状態を指します
- プロジェクトによってバージョンが違うなど色々障害があると思うので、それの対策です
- 簡単なアプリ作成
- VueのSPAサンプル
- Go(Echo)のREST APIサーバー
- (おまけ)クロスコンパイルしてWindows上でやまびこアプリを起動してみる
ここで書かないこと
- 本番環境のセッティング、CI/CD、テスト
- 本番環境を意識したリスク管理
- 言語の基礎説明
- Git、GitHubの基本的な使い方
※気が向いたら、時間をつくって別の記事に書くかもしれません
必要事項
- Docker・docker-composeが使えること
- Gitが使えること
- VSCodeをインストールしていること
各種バージョン(執筆時点)
- Windows10 Pro(1909)
- 2004であればHomeであってもWSL2を使ってできます
- Docker(19.03.8)
- Git(2.27.0)
- VSCode(1.47.2)
- VSCodeの拡張機能
- Docker(任意)
- Remote Container
- Go(コンテナ内部で利用)
- Vuter(コンテナ内部で利用)
(追記1)
コンテナ内部の設定やモジュールを用いて開発することができますので、ローカルにはGo言語もNodeJSもインストールしたりPathを通したりする必要がありません。(追記2)
Remote Containerは必須ではありませんが、とても便利な拡張機能です。
下図のように、ローカルからVSCodeのRemote Containerでそのコンテナの内部のファイルシステムにアクセスできるようになり、ローカルと変わらない感覚でコンテナ内開発を行うことができます。
※作ったサンプルはdocker-composeでマウント設定しているので、コンテナ内のソースコード修正がローカルにも反映されます。
こちらの画像の出典および詳細: https://code.visualstudio.com/docs/remote/containers
実際の作業
こんな感じですすめました。
- ゴール設定
- Git管理の開始
- 環境構築
- VueのSPA作成(Vue Cliでプロジェクト立ち上げまで)
- GoのやまびこAPIサーバーの作成
- SPAとAPIの連携
ゴール設定
Must
- Git管理できること
- ブラウザで入力した文字列を戻すだけのやまびこアプリができること
Optional
- 文字列をDBに登録できること
- 過去の入力したすべての文字列をブラウザで閲覧できること
(終わらなかったので、後日やります。たぶん。)
Git管理の開始
- Gitプロジェクトの作成
> mkdir vue-go-spa-sample > cd vue-go-spa-sample > git init Initialized empty Git repository in C:/xxxxxxxxxxxx/repository/aocm/vue-go-spa-sample/.git/ > git commit --allow-empty -m "first commit" [master (root-commit) 2df398f] first commit
- GitHubにリモートリポジトリの作成、Push
環境構築
- Vue用、Go用、MySQL用のDockerfileを作成して、docker-compose.ymlで一括管理
- ファイルの中身については割愛
- 基本的に枠だけ用意して、あとからポチポチメンテナンスしてこうというスタンスなDockerfileです
- 理想はcompose upのタイミングでなるべくすませるべきです。(作業者によって開発環境が変わらないようにするために、ある程度自動で準備ができるようにするべき)
> docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 6491b0eb8e96 vue-go-spa-sample_client "docker-entrypoint.s…" 9 minutes ago Up 8 minutes 0.0.0.0:8080->8080/tcp vgs-client 42d9f8f675b9 vue-go-spa-sample_server "bash" 27 minutes ago Up 8 minutes 0.0.0.0:8000->8000/tcp vgs-server e8a1de1c140d vue-go-spa-sample_db "docker-entrypoint.s…" 27 minutes ago Up 8 minutes 33060/tcp, 0.0.0.0:33060->3306/tcp vgs-dbVueのSPA作成
- RemoteContainerで立ち上げます
※画像のフォルダマークをクリックすると立ち上がります前述の通り、RemoteContainerは必須ではないですが、コンテナ内部で開発(ローカルを汚さない)ためにあると便利である拡張機能です。
以降は、基本的には各コンテナの中で作業しています
Vue Cliのインストール
/usr/src/app # npm install -g @vue/cliアプリケーションの作成
※それなりに時間がかかりますので、待っている間Goの方に着手しました。
※カスタマイズについては、好みとか目的によります。今回はRouterだけ使いたいが今後手を加えることを考えていろいろと好みで追加しています。/usr/src/app # vue create vue-spa Vue CLI v4.4.6 ? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, Router, Vuex, Linter, Unit ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes ? Pick a linter / formatter config: Airbnb ? Pick additional lint features: Lint on save ? Pick a unit testing solution: Jest ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? Nosuccess Saved lockfile. Done in 100.17s. ⚓ Running completion hooks... ? Generating README.md... ? Successfully created project my-project. ? Get started with the following commands: $ cd my-project $ yarn serve指示にしたがって起動してみます。
DONE Compiled successfully in 14361ms App running at: - Local: http://localhost:8080/Dockerのポートを解放しておいてあるので、ホストのほうで http://localhost:8080/ をブラウザで開いてみて、画像のように表示されていれば成功です。
GoでAPI作成
RemoteContainerで立ち上げます。コンテナ内部にVSCodeサーバーを立ち上げてリンクします。
立ち上がり次第、コンテナ内部のVSCodeでGoの拡張機能をインストールします
インストールしたら、Ctrl + Shift + P で command palette 開いて 「Go: Install/Update tools」 します。
すべてチェックして実行したら、下記のログがでるまで待ちます。
. . Installing github.com/sqs/goreturns SUCCEEDED Installing golang.org/x/lint/golint SUCCEEDED All tools successfully installed. You are ready to Go :).簡単なGoファイルを作って実行します。
package main import ( "fmt" ) func main() { fmt.Println("####### start #######") }root@42d9f8f675b9:/go/src/github.com/aocm/vue-go-spa-sample# go run main.go ####### start #######問題なく動きそうなので、EchoでRestAPIサーバーを立てます。
package main import ( "github.com/aocm/vue-go-spa-sample/handler" "github.com/labstack/echo" "github.com/labstack/echo/middleware" ) func main() { // Echoのインスタンス作る e := echo.New() // 全てのリクエストで差し込みたいミドルウェア(ログとか)はここ e.Use(middleware.CORS()) e.Use(middleware.Logger()) e.Use(middleware.Recover()) // ルーティング e.POST("/yamabiko", handler.YamabikoAPI()) e.OPTIONS("/yamabiko", handler.OptionsCheck()) // サーバー起動 e.Start(":8000") }package handler import ( "net/http" "github.com/labstack/echo" ) // YamabikoParam は /yamabiko が受けとるJSONパラメータを定義します。 type YamabikoParam struct { Message string `json:"message"` } // YamabikoAPI は /api/hello のPost時のJSONデータ生成処理を行います。 func YamabikoAPI() echo.HandlerFunc { return func(c echo.Context) error { param := new(YamabikoParam) if err := c.Bind(param); err != nil { return err } return c.JSON(http.StatusOK, map[string]interface{}{"hello": param.Message}) } }SPAとAPIの連携
VSCodeの拡張機能のVuterをインストールします
登録画面を作成
Yamabiko.vueをつくっていきます。
<template> <div> <h1>This is an Yamabiko page</h1> <input v-model="message" placeholder="Say Yahho"> <button @click="Send">Send</button> <p>Yamabiko : {{ yamabiko }}</p> </div> </template> <script> export default { name: 'Yamabiko', data() { return { message: '', yamabiko: '', }; }, methods: { async Send() { const yamabiko = await this.CallYamabikoAPI().then((res) => res.json()); this.yamabiko = yamabiko.message; window.alert(this.yamabiko); }, async CallYamabikoAPI() { const url = 'http://localhost:8000/yamabiko'; const data = { message: this.message, }; try { return await window.fetch(url, { method: 'POST', headers: { 'X-Requested-With': 'csrf', // csrf header 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); } catch (e) { console.log(e); return e; } }, }, }; </script>Mustの「ブラウザで入力した文字列を戻すだけのやまびこアプリができること」が達成できました!
おまけ
Linux向けにビルドして、コンテナ内部で実行してみる
root@f83b97e201a6:/go/src/github.com/aocm/vue-go-spa-sample# GOOS=linux GOARCH=amd64 go build main.go root@f83b97e201a6:/go/src/github.com/aocm/vue-go-spa-sample# ./main ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.1.16 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:8000フロントからも利用することができましたのでOKです。
Windows向けにビルドしてホストで実行してみる
portが被ると失敗しますので、番号を8001に変更した後ビルドしてみます
root@f83b97e201a6:/go/src/github.com/aocm/vue-go-spa-sample# GOOS=windows GOARCH=386 go build -o yamabiko.exe main.goWindowsで実行してみるとこのように窓が立ち上がり、実際に動くことが確認できます。
(簡単にクロスコンパイルするのほんとすごい。。。)
おわりに
久しぶりにプログラム書いたのでところどころ詰まるところがありました。
この記事とプログラムの考案・コーディングに合計8時間くらいかかってしまったので、学習を習慣づける・コーディング力を高める・利用できるアセットを増やすためにもう少し軽めに、情報量を狭く深くアウトプットしつづけたいなとも思いました。かなり改善点が多いコードなのでプロダクトに利用できる状態じゃないのも事実であり、アセット化するためにももっと時間をかけたいです。
それにしても、やりたいこと知りたいことに対して時間が圧倒的に足りないですね。
もし読者様の作業時短になれていれば幸いです。もし無駄なお時間になっていたらすみません。間違っていること、質問、アドバイスおよび応援コメントなどコメントいただけると幸いです。
参考
- 投稿日:2020-07-26T16:44:41+09:00
Vue.jsとGo言語で簡単なWebアプリを作成する
初投稿です。
過去の経験の復習かねて、書き起こしました。概要
成果物はこちらになりますので、VueやGoを始めてみたい方への力になれれば幸いです。
https://github.com/aocm/vue-go-spa-sample方針としては、なるべくローカルを汚さないことを最優先に考えました。
開発者ごとに環境が違うのも大変ですし、他のプロジェクト(自分の個人開発・仕事)で他のバージョンを利用していたら思わぬ影響が出てしまうのも嫌ですので、直接ローカルにインストールするのはなるべく避けたいです。今回はDockerを使って、コンテナ内部で開発環境を作って作業していきます。
ただ、Gitについてはローカル(ホスト)でしか利用できない環境になってしまったので、そこは課題です。
(最低限、更新差分はコンテナの中でも確認できるようにしたほうが親切でした。)対象読者
- Dockerは最低限環境構築できているけど、Webアプリ開発ってどこから手を付ければいいんだ?という人
- Goの簡単な動くサンプルが見たい人
- Vueの簡単な動くサンプルが見たい人
※きれいなコードを書くことが目的ではないのでご了承ください
書くこと
- なるべくローカルを汚さないような開発環境構築の方法
- ローカルを汚す...ここでは、各プラグインやソフトウェアのインストールしまくる状態を指します
- プロジェクトによってバージョンが違うなど色々障害があると思うので、それの対策です
- 簡単なアプリ作成
- VueのSPAサンプル
- Go(Echo)のREST APIサーバー
- (おまけ)クロスコンパイルしてWindows上でやまびこアプリを起動してみる
ここで書かないこと
- 本番環境のセッティング、CI/CD、テスト
- 本番環境を意識したリスク管理
- 言語の基礎説明
- Git、GitHubの基本的な使い方
※気が向いたら、時間をつくって別の記事に書くかもしれません
必要事項
- Docker・docker-composeが使えること
- Gitが使えること
- VSCodeをインストールしていること
各種バージョン(執筆時点)
- Windows10 Pro(1909)
- 2004であればHomeであってもWSL2を使ってできます
- Docker(19.03.8)
- Git(2.27.0)
- VSCode(1.47.2)
- VSCodeの拡張機能
- Docker
- RemoteContainer
- Go(コンテナ内部で利用)
- Vuter(コンテナ内部で利用)
実際の作業
こんな感じですすめました。
- ゴール設定
- Git管理の開始
- 環境構築
- VueのSPA作成(Vue Cliでプロジェクト立ち上げまで)
- GoのやまびこAPIサーバーの作成
- SPAとAPIの連携
ゴール設定
Must
- Git管理できること
- ブラウザで入力した文字列を戻すだけのやまびこアプリができること
Optional
- 文字列をDBに登録できること
- 過去の入力したすべての文字列をブラウザで閲覧できること
(終わらなかったので、後日やります。たぶん。)
Git管理の開始
- Gitプロジェクトの作成
> mkdir vue-go-spa-sample > cd vue-go-spa-sample > git init Initialized empty Git repository in C:/xxxxxxxxxxxx/repository/aocm/vue-go-spa-sample/.git/ > git commit --allow-empty -m "first commit" [master (root-commit) 2df398f] first commit
- GitHubにリモートリポジトリの作成、Push
環境構築
- Vue用、Go用、MySQL用のDockerfileを作成して、docker-compose.ymlで一括管理
- ファイルの中身については割愛
- 基本的に枠だけ用意して、あとからポチポチメンテナンスしてこうというスタンスなDockerfileです
- 理想はcompose upのタイミングでなるべくすませるべきです。(作業者によって開発環境が変わらないようにするために、ある程度自動で準備ができるようにするべき)
> docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 6491b0eb8e96 vue-go-spa-sample_client "docker-entrypoint.s…" 9 minutes ago Up 8 minutes 0.0.0.0:8080->8080/tcp vgs-client 42d9f8f675b9 vue-go-spa-sample_server "bash" 27 minutes ago Up 8 minutes 0.0.0.0:8000->8000/tcp vgs-server e8a1de1c140d vue-go-spa-sample_db "docker-entrypoint.s…" 27 minutes ago Up 8 minutes 33060/tcp, 0.0.0.0:33060->3306/tcp vgs-dbVueのSPA作成
- RemoteContainerで立ち上げます(必須ではないですが、ローカルを汚さない目的です。)
※画像のフォルダマークをクリックすると立ち上がりますVue Cliのインストール
/usr/src/app # npm install -g @vue/cliアプリケーションの作成
※それなりに時間がかかりますので、待っている間Goの方に着手しました。
※カスタマイズについては、好みとか目的によります。今回はRouterだけ使いたいが今後手を加えることを考えていろいろと好みで追加しています。/usr/src/app # vue create vue-spa Vue CLI v4.4.6 ? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, Router, Vuex, Linter, Unit ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes ? Pick a linter / formatter config: Airbnb ? Pick additional lint features: Lint on save ? Pick a unit testing solution: Jest ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? Nosuccess Saved lockfile. Done in 100.17s. ⚓ Running completion hooks... ? Generating README.md... ? Successfully created project my-project. ? Get started with the following commands: $ cd my-project $ yarn serve指示にしたがって起動してみます。
DONE Compiled successfully in 14361ms App running at: - Local: http://localhost:8080/Dockerのポートを解放しておいてあるので、ホストのほうで http://localhost:8080/ をブラウザで開いてみて、画像のように表示されていれば成功です。
GoでAPI作成
RemoteContainerで立ち上げます。コンテナ内部にVSCodeサーバーを立ち上げてリンクします。
こちらの画像の出典および詳細: https://code.visualstudio.com/docs/remote/containers以降は、基本的には各コンテナの中で作業しています
立ち上がり次第、コンテナ内部のVSCodeでGoの拡張機能をインストールします
インストールしたら、Ctrl + Shift + P で command palette 開いて 「Go: Install/Update tools」 します。
すべてチェックして実行したら、下記のログがでるまで待ちます。
. . Installing github.com/sqs/goreturns SUCCEEDED Installing golang.org/x/lint/golint SUCCEEDED All tools successfully installed. You are ready to Go :).簡単なGoファイルを作って実行します。
package main import ( "fmt" ) func main() { fmt.Println("####### start #######") }root@42d9f8f675b9:/go/src/github.com/aocm/vue-go-spa-sample# go run main.go ####### start #######問題なく動きそうなので、EchoでRestAPIサーバーを立てます。
package main import ( "github.com/aocm/vue-go-spa-sample/handler" "github.com/labstack/echo" "github.com/labstack/echo/middleware" ) func main() { // Echoのインスタンス作る e := echo.New() // 全てのリクエストで差し込みたいミドルウェア(ログとか)はここ e.Use(middleware.CORS()) e.Use(middleware.Logger()) e.Use(middleware.Recover()) // ルーティング e.POST("/yamabiko", handler.YamabikoAPI()) e.OPTIONS("/yamabiko", handler.OptionsCheck()) // サーバー起動 e.Start(":8000") }package handler import ( "net/http" "github.com/labstack/echo" ) // YamabikoParam は /yamabiko が受けとるJSONパラメータを定義します。 type YamabikoParam struct { Message string `json:"message"` } // YamabikoAPI は /api/hello のPost時のJSONデータ生成処理を行います。 func YamabikoAPI() echo.HandlerFunc { return func(c echo.Context) error { param := new(YamabikoParam) if err := c.Bind(param); err != nil { return err } return c.JSON(http.StatusOK, map[string]interface{}{"hello": param.Message}) } }SPAとAPIの連携
VSCodeの拡張機能のVuterをインストールします
登録画面を作成
Yamabiko.vueをつくっていきます。
<template> <div> <h1>This is an Yamabiko page</h1> <input v-model="message" placeholder="Say Yahho"> <button @click="Send">Send</button> <p>Yamabiko : {{ yamabiko }}</p> </div> </template> <script> export default { name: 'Yamabiko', data() { return { message: '', yamabiko: '', }; }, methods: { async Send() { const yamabiko = await this.CallYamabikoAPI().then((res) => res.json()); this.yamabiko = yamabiko.message; window.alert(this.yamabiko); }, async CallYamabikoAPI() { const url = 'http://localhost:8000/yamabiko'; const data = { message: this.message, }; try { return await window.fetch(url, { method: 'POST', headers: { 'X-Requested-With': 'csrf', // csrf header 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); } catch (e) { console.log(e); return e; } }, }, }; </script>Mustの「ブラウザで入力した文字列を戻すだけのやまびこアプリができること」が達成できました!
おまけ
Linux向けにビルドして、コンテナ内部で実行してみる
root@f83b97e201a6:/go/src/github.com/aocm/vue-go-spa-sample# GOOS=linux GOARCH=amd64 go build main.go root@f83b97e201a6:/go/src/github.com/aocm/vue-go-spa-sample# ./main ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.1.16 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:8000フロントからも利用することができましたのでOKです。
Windows向けにビルドしてホストで実行してみる
portが被ると失敗しますので、番号を8001に変更した後ビルドしてみます
root@f83b97e201a6:/go/src/github.com/aocm/vue-go-spa-sample# GOOS=windows GOARCH=386 go build -o yamabiko.exe main.goWindowsで実行してみるとこのように窓が立ち上がり、実際に動くことが確認できます。
(簡単にクロスコンパイルするのほんとすごい。。。)
おわりに
久しぶりにプログラム書いたのでところどころ詰まるところがありました。
この記事とプログラムの考案・コーディングに合計8時間くらいかかってしまったので、学習を習慣づける・コーディング力を高める・利用できるアセットを増やすためにもう少し軽めに、情報量を狭く深くアウトプットしつづけたいなとも思いました。かなり改善点が多いコードなのでプロダクトに利用できる状態じゃないのも事実であり、アセット化するためにももっと時間をかけたいです。
それにしても、やりたいこと知りたいことに対して時間が圧倒的に足りないですね。
もし読者様の作業時短になれていれば幸いです。もし無駄なお時間になっていたらすみません。間違っていること、質問、アドバイスおよび応援コメントなどコメントいただけると幸いです。
参考
- 投稿日:2020-07-26T16:13:10+09:00
【Vue】リロードしても大丈夫。そう、vuex-persistedstateならね。
Vuexで管理しているステートをリロードしても消えないようにする
vuex-persistedstateというVeux関連パッケージの紹介記事です。Vuexのステートをすでに同様の記事はありますが、備忘録として自分でまとめた物を残しておきます。
Vuexではリロードするとステートは初期状態にもどってしまう
VuexはVueインスタンスがもつステート(データ)をストアと呼ばれる管理場所で一括管理して、全てのコンポーネント間でステートを簡易に共有する仕組みを提供してくれます。つまり、あるコンポーネントでステートの値を変更して、他のコンポーネントでそのステートを使うことができます。
しかし、メモリ上に保存されたVeuxのステートはブラウザを閉じたり、リロードしたりすると初期状態にリセットされてしまいます。
例えばVuexを使って入力フォームの値やユーザーが選択したセレクトボックスの値などを管理していた場合、誤って途中でブラウザを閉じたりリロードしたりするとユーザーが入力した値は消えてしまいます。
このようなケースではUX向上のためにステートを維持させた方が良いでしょう。
このようなケースにはvuex-persistedstateを使うと、VeuxのステートをWebストレージに一旦逃して、リロードした際に読み込んでステートを維持させることが可能です。
vuex-persistedstateを使う
詳しくはこちらを参照。
nuxtで使う時はpluginとして設定が必要なようです。
インストール
yarnまたはnpmでインストール出来ます。
# yarn yarn add vuex-persistedstate # npm npm install --save vuex-persistedstate使い方
まず、ストアをモジュールで分けている場合は、vuexの設定で
modules
プロパティに管理対象のモジュールをセットしておく必要があります。(ここでは例としてauth
とmaster
というモジュールがあるとします)。大元のVuexの設定ファイル(ここではindex.js)でストアのセッティングをするところにvuex-persistedstateの設定を書いていきます。vuex-persistedstateの設定はVuexの設定の
plugins
プロパティに書いていきます。最低限設定しておいた方がいいのはkey
,paths
,storage
プロパティかなと思います。(なくても動くには動く)
key
はストレージに保存する時のプロパティ名です。デフォルトではvuex
ですが、他のアプリとかぶる可能性があるのでユニークなものに変えましょう。
paths
に管理対象のステートを書きます。値は<モジュール名>.<ステート>
と書いて、配列に入れます。配列に入れたステートが管理対象になります。pathsを書かなかった場合はデフォルトで全てのステートが対象になります。
storage
には保存するストレージを指定します。Webストレージ(ローカルストレージ、セッションストレージ)とCookieから選べるようです。他にもプロパティが色々あるようです。全プロパティはここから確認出来ます。
次は設定の一例。
frontend/stores/index.jsimport Vue from 'vue' import Vuex from 'vuex' import createPersistedState from 'vuex-persistedstate' import auth from './auth.js' import master from './master.js' Vue.use(Vuex) // Vuexの設定 export default new Vuex.Store({ // ストアをモジュールに分けている場合。vuexのモジュールを指定 modules: { auth, master, }, // `createPersistedState()`でインスタンス作成。引数に設定を書く plugins: [createPersistedState( { // ストレージのキーを指定。デフォルトではvuex key: 'anyGreatApp', // 管理対象のステートを指定。pathsを書かない時は`modules`に書いたモジュールに含まれるステート全て。`[]`の時はどれも保存されない paths: [ 'auth.isLoggedIn', 'master.dataSelected' ], // ストレージの種類を指定する。デフォルトではローカルストレージ storage: window.sessionStorage } )] })保存先の違い
storage
の違いは次の通りです。使用用途によって、変えるといいと思います。複数タブでの作業を想定している場合はセッションストレージを選択するといいと思います。タブごとに異なるセッションとして扱われるので、ステートもタブごとに管理出来ます。細かい違いはCookieとWebStorageとSessionについてのまとめを参照。
指定できるストレージ storage
プロパティの値保存期間の違いなど ローカルストレージ window.localStrage
意図的に消さない限りずっと残る セッションストレージ window.sessionStrage
セッションが切れたらorタブやブラウザを閉じたらリセット Cookie こちらを参照 期限を任意に設定できる&容量が小さい(4KB)& HTTP/HTTPSでサーバーに送信される また、ローカルストレージのデータを暗号化できるパッケージと組み合わせてセキュアに使うこともできるようです。ハッキングされて流出するとまずいようなデータを扱う時はいいかもしれません。
(そもそも、そういったデータは本来Web StorageやCookieに保存すべきではないですが)注意点
ログアウト時に画面のリロードでステートを初期状態にリセットするようにしている場合は、このパッケージを使うと当然リセットは効かなくなります。
その場合、ログアウトしてもステートはそのままになるので、違うユーザーでログインしても以前のユーザーのステートが残ったままになってしまいます。
ステートをリセットするアクション・ミューテーションを作っておいて、ログアウト時に呼び出すようにしておくと良さそうです
(他にステートをまとめてリセットするいい方法があったら教えてください)
- 投稿日:2020-07-26T14:27:45+09:00
【爆速】VueCLI × netlify でWebアプリを公開する
この記事について
この4連休、自粛生活で暇を持て余しているエンジニアマンです。
暇すぎてブログを立ち上げてみようと思いました。ブログを立ち上げようとするのはこれで2回目です。
1回目はWordPressで立ち上げました。
でも個人的にWordPressの立ち上げは面倒くさい!
レンタルサーバを借りるのにお金かかるし、もっと楽にお金かけずに自分だけのサイトを立ち上げる方法ないかなー。そこで見つけたのが、netlifyというホスティングサービスです。
netlifyを使えば、爆速でブログのようなwebページを立ち上げることができます。
WordPressとちがってフロントを自分から作っていく必要はありますが、何より無料で立ち上げられるというのが気軽でいい!!
今回はVueCLIで立ち上げたサンプルページをnetlifyで爆速公開させていきたいと思います。VueCLIでプロジェクトを立ち上げる
vueCLIでプロジェクト立ち上げまでの流れです
VueCLIインストール
割愛
プロジェクト作成
vue create [プロジェクト名]ローカルで立ち上げてみる
yarn serveすると次のデモページが表示されるはずです。
今回はこちらのページをnetlifyで公開させてみます。netlifyでアプリを公開
githubにローカルリポジトリをプッシュしておく
Githubに連携することで、リモートへのプッシュと同時に勝手にサイトを更新してくれるようになります!(便利すぎる)
ついでに連携させてしまいましょう
netlifyサービスに登録
netlifyの公式ページでgithub連携を選びアカウント登録します。
新しいサイトを作成
画像の赤枠ボタンを押して、ホスティングさせるリポジトリを選択しましょう。
デプロイ設定
github連携を選択肢し、
画像の設定画面で、
・ブランチ(branch to deploy)
・ビルドコマンド(build command)
・公開するディレクトリ(publish directory)
を設定します。ビルドコマンドは yarn buildに
公開するディレクトリはdistに設定します。公開完了
するとデプロイ作業が自動で始まります。
完了するまで少しだけ時間がかかるので待機しましょう。
デプロイ作業が完了すると、サイトのURLが生成されるのでそちらへアクセスするとVueのデモページが表示されます。
これでブログ作成のスタート地点に立てました!!
- 投稿日:2020-07-26T14:06:38+09:00
Firebase App named '[DEFAULT]' already existsというエラーの解決方法
nuxt.jsでfirebaseと連携したら表題のエラーが出ました。
①firebase.jsフォルダを開きます
多分こんな感じファイルになってるかと。
firebase.jsimport Vue from "vue" import { firestorePlugin } from "vuefire" import firebase from "firebase" import "firebase/firestore" Vue.use(firestorePlugin) firebase.initializeApp({ apiKey: "うんたら", authDomain: "うんたら.firebaseapp.com", databaseURL: "https://うんたら.firebaseio.com", projectId: "うんたら", storageBucket: "うんたら.appspot.com", messagingSenderId: "123456789", appId: "1:123456789:web:123456789", measurementId: "うんたら", }) export const db = firebase.firestore() export const auth = firebase.auth()②データを囲む
上記のfirebase.initializeApp({})をif(!firebase.apps.length){}で囲みます。
以下のようになります。
firebase.jsimport Vue from "vue" import { firestorePlugin } from "vuefire" import firebase from "firebase" import "firebase/firestore" Vue.use(firestorePlugin) if (!firebase.apps.length) { firebase.initializeApp({ apiKey: "うんたら", authDomain: "うんたら.firebaseapp.com", databaseURL: "https://うんたら.firebaseio.com", projectId: "うんたら", storageBucket: "うんたら.appspot.com", messagingSenderId: "123456789", appId: "1:123456789:web:123456789", measurementId: "うんたら", }) } export const db = firebase.firestore() export const auth = firebase.auth()これでエラーが解決してるはずです。
その他
↓参考URL↓
Nuxt + Firebase: Firebase App named '[DEFAULT]' already exists (app/duplicate-app)
https://stackoverflow.com/questions/53887447/nuxt-firebase-firebase-app-named-default-already-exists-app-duplicate-ap
- 投稿日:2020-07-26T13:23:28+09:00
AWS CDKとGitHub ActionsでVueのWebページを環境別デプロイ
このディレクトリ構成で、AWSへのデプロイフローを作る。
Vue.jsのWebサイトを本番 (prod)
/ステージング (stg)
環境に分けて管理する。ステージング環境は検証用として使える。v2okimochi-dev/ ├ .gitub/ │ └ workflows/ │ └ cdk.yml (GitHub Actionsワークフローを定義するymlファイルを置く) │ ├ frontend/ │ ├ staging_assets/ │ │ └ robots.txt │ └ ... (ここにVueのアレコレを置く) │ ├ batch/ │ └ signed_url/ │ └ ... (ここに証明書付きURL作成用のPythonバッチアレコレを置く) │ └ infra/ └ aws-cdk/ └ ... (ここにAWS CDKのアレコレを置く)ほとんどのAWSサービスは AWS CDK (docsはコレ)でデプロイする。CDKの性質上、CloudFormationを軸に管理されるみたい。
CDKによるデプロイ部分は、 (local上でも良いが、) GitHub Actionsに定義して developブランチにpush (merge)されたらステージング環境へデプロイし、masterブランチにpush (merge)されたら本番環境へデプロイするような自動デプロイフローを作る。
全体としては、こうなる
- S3
frontend/dist/
を置く場所- CloudFront以外からアクセスさせる気はないのでOrigin Access Identityだけ許す
- ステージング環境だけ
robots.txt
も置く
- 念のため検索ロボットによる捕捉も防ぐ
- CloudFront
- S3内のファイルは全てCloudFrontを通してアクセスさせる
- HTTPS通信だけ許す
- 本番環境だけ、予め取得しておいたAWSのパブリック
SSL/TLS証明書
&独自のドメイン名を設定する
- 詳細はそれぞれRoute53, Certificate Managerの項目に書いた
- デフォルトではAWSによってCloudFront用のドメイン名と証明書が付与される
- ステージング環境だけ、
index.html
へのアクセスは署名付きURL認証を差し込む (公開されても嬉しくないので)
- 全ファイルに差し込みたかったが
index.html
以外が403を返してしまい、原因もわからず泣く泣く妥協index.html
以外のファイルはURLがバレれば未認証でもアクセスできてしまうのがだいぶアレ- Route53
- 本番環境だけ、独自ドメインを含むドメイン名 (たとえば
www.mydomain.com
)でCloudFrontへ繋ぐ- 今回は
お名前.com
(外部)で登録したドメイン名なので、お名前.com
側に 予めRoute53で作ったホストゾーン内のネームサーバーを登録しておく- CDKではAレコード・AAAAレコードの作成をやる (FQDNとCloudFrontドメイン名の紐付け)
- Certificate Manager
- AWSのパブリック
SSL/TLS証明書
を取得する- リージョンは
バージニア北部 (us-east-1)
で作成する (CloudFrontとの結びつけがここしか対応してなさそう)- 予めRoute53に
CNAME
レコードとして登録しておくAWS側で予めやっておく手動作業
全部cdkでやりたかったけど。。。
AWS外で登録した独自ドメインをAWS Route53で管理
たとえば お名前.comで独自ドメイン
mydomain.com
を登録した場合。
- Route53でパブリックホストゾーン
mydomain.com
を作成
このホストゾーンIDは後で使うのでメモしておく- お名前.comで独自ドメイン
mydomain.com
のネームサーバーを、 "1.によりホストゾーンに自動生成された NSレコードmydomain.com
のネームサーバー"に変更AWSでSSL/TLS証明書を取得する
- Certificate Managerでパブリック証明書
*.mydomain.com
をリクエストする
リクエストが作られ、ステータスが検証保留中
となるはず- 証明書のCNAMEレコードをRoute53のホストゾーン
mydomain.com
に追加する
(Route53に追加
みたいなボタンを押せば自動で追加してくれる)- 証明書のARNをメモしておく (CloudFrontとの紐付けをcdkでやるため)
local上での作業
shellで手作業
CDKの導入は Getting started with the AWS CDKに従った。
あとCDKは全部TypeScriptで書いた。
必要なライブラリのインストールnpm install @aws-cdk/aws-s3 @aws-cdk/aws-s3-deployment @aws-cdk/aws-cloudfront @aws-cdk/aws-certificatemanager @aws-cdk/aws-route53 @aws-cdk/aws-route53-targets
shell:AWSアカウントIDとリージョンを指定して `CDKTookKit`というCloudFormation スタックを作成
cdk bootstrap XXXXXXXXXXXX/ap-northeast-1
AWS CDKで利用するみたい。作成してないと、こういうエラーを吐かれる。。。
エラー文❌ v2okimochi-dev failed: Error: This stack uses assets, so the toolkit stack must be deployed to the environment (Run "cdk bootstrap aws://unknown-account/unknown-region")cf. AWS CDKの'aws-s3-deployment'を使ってクライアントサイドも一緒にデプロイする
AWS CDKのstackを記述
各ソースコード (いっぱいあるので折りたたんだ)
aws-cdk/bin/aws-cdk.ts#!/usr/bin/env node import 'source-map-support/register'; import * as cdk from '@aws-cdk/core'; import { AwsCdkStack } from '../lib/stacks/v2okimochi-dev'; import * as util from "../lib/util"; const validateEnvironment = (target: string) => { /** * 意図した環境が `--context`オプションで指定されていない場合、例外終了させる */ if (!target) throw new Error("環境が指定されていません"); const validTarget = util.findEnvironment(target); if (!validTarget) throw new Error(`環境名が正しくありません: ${target}`); return validTarget; }; const productName = "v2okimochi-dev"; const app = new cdk.App(); const target = validateEnvironment(app.node.tryGetContext("target")) new AwsCdkStack(app, `${productName}-${target}`, target);aws-cdk/lib/util.ts/** * プロダクト環境名の定義 */ export enum Environments { PROD = "prod", STG = "stg", } export function findEnvironment(env: string): string | undefined { if (env == Environments.PROD) return Environments.PROD; else if (env == Environments.STG) return Environments.STG; else return undefined; } /** * 環境変数の取り出し */ export function validateEnvironmentVariable(key: string): string { const value = process.env[key]; if (!value) throw new Error(`環境変数の値が見つかりません: ${key}`); return value; }aws-cdk/lib/stacks/v2okimochi-dev.tsimport * as cdk from "@aws-cdk/core"; import * as certificatemanager from "@aws-cdk/aws-certificatemanager"; import * as s3 from "@aws-cdk/aws-s3"; import * as s3deploy from "@aws-cdk/aws-s3-deployment"; import { Environments, validateEnvironmentVariable } from "../util"; import { setupCloudFront } from "../services/cloudfront/setupCloudFront"; import { setupRoute53 } from "../services/route53/setupRoute53"; export class AwsCdkStack extends cdk.Stack { constructor( scope: cdk.Construct, id: string, target: string, props?: cdk.StackProps ) { super(scope, id, props); /** ############################## * S3 Bucket */ const s3WebName = `${id}-web`; const s3Web = new s3.Bucket(this, id, { bucketName: s3WebName, removalPolicy: cdk.RemovalPolicy.DESTROY, }); // Prod環境以外では、Webクローラに登録させないためのrobots.txtもデプロイする const s3WebAssets: s3deploy.ISource[] = target === Environments.PROD ? [s3deploy.Source.asset("../../frontend/dist")] : [ s3deploy.Source.asset("../../frontend/dist"), s3deploy.Source.asset("../../frontend/staging_assets"), ]; new s3deploy.BucketDeployment(this, `${s3WebName}-deployment`, { sources: s3WebAssets, destinationBucket: s3Web, }); /** ############################## * CloudFront, Certificate Manager */ const validDomain = validateEnvironmentVariable( "V2OKIMOCHI_DEV_PROD_DOMAIN" ); const validSubDomain = validateEnvironmentVariable( "V2OKIMOCHI_DEV_PROD_SUBDOMAIN" ); const fqdn = `${validSubDomain}.${validDomain}`; const arn = validateEnvironmentVariable( "V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN" ); const validCertificate = certificatemanager.Certificate.fromCertificateArn( this, `${id}-acm-certificate`, arn ); const cloudFront = new setupCloudFront(this, { id: `${id}-cloudfront`, target: target, s3WebBucket: s3Web, certificate: validCertificate, fqdns: [fqdn], }); /** ############################## * Route53 */ const route53 = target === Environments.PROD ? new setupRoute53(this, { id: `${id}-route53`, target: target, domain: validDomain, subDomain: validSubDomain, cloudFront: cloudFront, }) : undefined; /** ############################## * Tag */ cdk.Tag.add(this, "Product", id); } }aws-cdk/lib/services/cloudfront/setupCloudFront.tsimport * as cdk from "@aws-cdk/core"; import * as certificatemanager from "@aws-cdk/aws-certificatemanager"; import * as cloudfront from "@aws-cdk/aws-cloudfront"; import * as s3 from "@aws-cdk/aws-s3"; import { Environments, validateEnvironmentVariable } from "../../util"; export interface setupCloudFrontProps { /** * リソースの共通名 (たとえばプロダクト名) */ readonly id: string; /** * 環境 (Prodなど) */ readonly target: string; /** * オリジンとして指定するS3 Bucket */ readonly s3WebBucket: s3.Bucket; /** * SSL/TLS証明書を取得したAWS Certificate Manager */ readonly certificate: certificatemanager.ICertificate; /** * CloudFrontのカスタムFQDNリスト */ readonly fqdns: string[]; } export class setupCloudFront { private cloudFrontWebDistribution: cloudfront.CloudFrontWebDistribution; public distribution(): cloudfront.CloudFrontWebDistribution { return this.cloudFrontWebDistribution; } constructor(context: cdk.Stack, props: setupCloudFrontProps) { /** ############################## * 本番 (Prod)以外の環境では、公開しないようにデプロイする */ const signedAccount = validateEnvironmentVariable( "V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID" ); // Prod環境以外では署名付きURLを使用する const behaviorsWithEnv: cloudfront.Behavior[] = props.target === Environments.PROD ? [ { isDefaultBehavior: true, compress: true, minTtl: cdk.Duration.seconds(0), maxTtl: cdk.Duration.days(0), defaultTtl: cdk.Duration.days(0), }, ] : [ { isDefaultBehavior: false, pathPattern: "/index.html", trustedSigners: [signedAccount], compress: true, minTtl: cdk.Duration.seconds(0), maxTtl: cdk.Duration.days(0), defaultTtl: cdk.Duration.days(0), }, { isDefaultBehavior: true, compress: true, minTtl: cdk.Duration.seconds(0), maxTtl: cdk.Duration.days(0), defaultTtl: cdk.Duration.days(0), }, ]; // Prod環境以外では403を200にリダイレクトさせる const errorConfigurationsWithEnv: cloudfront.CfnDistribution.CustomErrorResponseProperty[] = props.target === Environments.PROD ? [ { errorCode: 403, errorCachingMinTtl: 0, responseCode: 200, responsePagePath: "/index.html", }, ] : []; // Prod環境にだけ独自ドメイン名を割り当てる const viewerCertificateWithEnv: cloudfront.ViewerCertificate | undefined = props.target === Environments.PROD ? { aliases: props.fqdns, props: { acmCertificateArn: props.certificate.certificateArn, sslSupportMethod: "sni-only", }, } : undefined; // CloudFrontだけがS3にアクセスできるようにする (ユーザはS3に直接アクセスできない) const oai = new cloudfront.OriginAccessIdentity( context, `${props.id}-oai`, { comment: "s3 access.", } ); this.cloudFrontWebDistribution = new cloudfront.CloudFrontWebDistribution( context, `${props.id}-cloudfront`, { defaultRootObject: "index.html", viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, viewerCertificate: viewerCertificateWithEnv, httpVersion: cloudfront.HttpVersion.HTTP2, priceClass: cloudfront.PriceClass.PRICE_CLASS_200, originConfigs: [ { s3OriginSource: { s3BucketSource: props.s3WebBucket, originAccessIdentity: oai, }, behaviors: behaviorsWithEnv, }, ], geoRestriction: { restrictionType: "whitelist", locations: ["JP"], }, errorConfigurations: errorConfigurationsWithEnv, } ); } }aws-cdk/lib/services/route53/setupRoute53.tsimport * as cdk from "@aws-cdk/core"; import * as route53 from "@aws-cdk/aws-route53"; import * as route53targets from "@aws-cdk/aws-route53-targets"; import { setupCloudFront } from "../../services/cloudfront/setupCloudFront"; import { validateEnvironmentVariable } from "../../util"; export interface setupRoute53Props { /** * リソースの共通名 (たとえばプロダクト名) */ readonly id: string; /** * 環境 (Prodなど) */ readonly target: string; /** * ドメイン名 */ readonly domain: string; /** * サブドメイン名 */ readonly subDomain: string; /** * CloudFront distribution */ readonly cloudFront: setupCloudFront; } export class setupRoute53 { constructor(context: cdk.Stack, props: setupRoute53Props) { const fqdn = `${props.subDomain}.${props.domain}`; const hostedZoneId = validateEnvironmentVariable( "V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID" ); const hostedZone = route53.HostedZone.fromHostedZoneAttributes( context, `${props.id}-hosted-zone`, { zoneName: props.domain, hostedZoneId: hostedZoneId, } ); const aRecord = new route53.ARecord(context, `${props.id}-a-record`, { zone: hostedZone, recordName: fqdn, target: route53.RecordTarget.fromAlias( new route53targets.CloudFrontTarget(props.cloudFront.distribution()) ), }); const aaaaRecord = new route53.AaaaRecord( context, `${props.id}-aaaa-record`, { zone: hostedZone, recordName: fqdn, target: route53.RecordTarget.fromAlias( new route53targets.CloudFrontTarget(props.cloudFront.distribution()) ), } ); } }アカウントIDとか、ハードコーディングするとアレなやつは環境変数として埋め込むようにした。
- V2OKIMOCHI_DEV_PROD_DOMAIN
- 本番環境で使う独自ドメイン
- 今回は
mydomain.com
みたいなやつ- V2OKIMOCHI_DEV_PROD_SUBDOMAIN
- 本番環境で使うサブドメイン
- たとえば
www
(FQDNにすればwww.mydomain.com
になる)- V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID
- 予め作っておいたRoute53ホストゾーンのID
- V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN
- 予め作っておいたCertificate Manager SSL/TLS証明書のARN
- V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID
- 署名済みユーザとして登録するアカウントID
- 今回は自分のアカウントIDだけ
たとえばこうやってCDKコマンドを使う。
diffで差分と文法エラーを確認、deployで実際にデプロイ、destroyで該当スタックを削除する。aws-cdk/# 本番 $ cdk diff --context target=prod # ステージング $ cdk diff --context target=stg実行時の引数でスタックIDごと変えることによって、本番環境とステージング環境を分けるようにした。
S3の設定
aws-cdk/lib/stacks/v2okimochi-dev.ts
に書いた。
removalPolicy
をDESTROY
にする
- CloudFormationスタックが削除される時S3バケットも削除するため
cf. [AWS CDK(Cloud Development Kit)] スタック削除時にS3バケットも削除されるように設定する- ただしS3バケットが空でないと削除できずにエラー失敗するので、事前にバケットを空にしておく必要がある
cf. Automatically delete an S3 bucket with the AWS CDK stackCloudFrontの設定
aws-cdk/lib/services/cloudfront/setupCloudFront.ts
に書いた。
本番/ステージングの環境ごとに設定を用意したりして長くなったのでファイルを分けている。
- SSL/TLS証明書を予め手作業で (Certificate Managerに)作っておき、そのARNからCDKで参照する
trustedSigners
にアカウントIDを指定することで、署名付きURLでのアクセス時にそのアカウントIDで持っているCloudFrontキーペアで照合されるっぽい
- 自分のアカウントIDだけ指定した場合、CloudFront管理画面上では
self
と表示される- Route53のレコードでも紐付けるが、CloudFront側にもaliasとして独自ドメイン名を付ける (
CNAMEs
列に表示される)Route53の設定
aws-cdk/lib/services/route53/setupRoute53.ts
に書いた。
本番環境でだけ作成する (条件分岐はaws-cdk/lib/stacks/v2okimochi-dev.ts
のほう)
- ホストゾーンは予め手作業で作っておき、そのID (
HostedZoneId
)からCDKで参照する- CDKで作るのはAレコードとAAAAレコード
- どちらもCloudFrontディストリビューションと独自ドメイン名を紐付ける
- ディストリビューションはCDK内で作成したインスタンスから拾う
- もしCDKでやらずにWebから操作するなら、紐付け対象を
CloudFront配信
、リージョンをバージニア北部
にしてCloudFrontドメイン名のほうを貼り付ければ良い (選択候補には出ないので手動で貼り付ける)Tagの設定
aws-cdk/lib/stacks/v2okimochi-dev.ts
の最後にしれっと書いている。
どうせCloudFormationによって管理されてるけど。。。署名付きURLの作成
(手動だけど。。。)
/batch/signed_url/
に、 ステージング環境のWebページにアクセスするための署名付きURLを作成するPythonスクリプトを書いた。そんな頻繁に定期実行するわけじゃないからバッチと呼ぶことには諸説ありそうだが。。。
AWS側での事前準備
署名済みユーザ (今回は自分のアカウントID)でCloudFrontキーペアを作り、プライベートキー (
.pem
)をダウンロードする。
rootユーザとしてログインしないと作れないみたい。cf. 署名付き URL と署名付き Cookie (信頼された署名者) の作成が可能な AWS アカウントの指定
スクリプト作成と実行
pipenvで適当にpython3.8の仮想環境を整えて、 boto3をimportして使う。
signed_url/Pipfile[[source]] name = "pypi" url = "https://pypi.org/simple" verify_ssl = true [packages] boto3 = "==1.4.4" [dev-packages] flake8 = "==3.8.3" [requires] python_version = "3.8.3" [scripts] flake8 = "flake8 --ignore E501 ."([dev-packages]と[scripts]に関する記述はAWSとは無関係なので無くても良い)
公式docsに従い、署名付きURL生成のコードを書く。
signed_url/src/main.pyimport datetime import os from botocore.signers import CloudFrontSigner from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding def rsa_signer(message): private_pem_path = os.getenv( 'V2OKIMOCHI_DEV_STG_CLOUDFRONT_PRIVATE_PEM_PATH') with open(private_pem_path, 'rb') as key_file: private_key = serialization.load_pem_private_key( key_file.read(), password=None, backend=default_backend()) return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1()) def handler(): key_id = os.getenv('V2OKIMOCHI_DEV_STG_CLOUDFRONT_KEY_ID') url = os.getenv('V2OKIMOCHI_DEV_STG_CLOUDFRONT_URL') expire_date = datetime.datetime(2020, 7, 28) cloudfront_signer = CloudFrontSigner(key_id, rsa_signer) # Create a signed url that will be valid until the specfic expiry date # provided using a canned policy. signed_url = cloudfront_signer.generate_presigned_url( url, date_less_than=expire_date) print(signed_url) if __name__ == "__main__": handler()cf. Boto3 Docs - Generate a signed URL for Amazon CloudFront
ハードコーディングがアレな値は環境変数にした。
- V2OKIMOCHI_DEV_STG_CLOUDFRONT_PRIVATE_PEM_PATH
- ダウンロードしたCloudFrontプライベートキー (
.pem
)へのパス- たとえば
/batch/signed_url/
に置いたなら値は./xxxxx.pem
みたいになる- V2OKIMOCHI_DEV_STG_CLOUDFRONT_KEY_ID
- pemファイル名に含まれているkey ID (大文字で20字くらいあるアレ)
- V2OKIMOCHI_DEV_STG_CLOUDFRONT_URL
https://xxxxxxxxxxx.cloudfront.net/index.html
みたいになるスクリプトで署名付きURLを発行
この
main.py
を実行して、署名付きURLを発行する。
今回はpipenvでPython仮想環境を作ったのでpipenv run
でやる/batch/signed_url/$ pipenv run python src/main.py
- 上記コードでは
2020/7/28
まで有効なURLを発行する- print文で標準出力されたURLを (改行は取り除いて)使う
- このへんもガチで自動化しようとするなら、コードをLambda化してCognitoやAPI Gatewayと連携させてログインページ作るくらいのことが必要そう
- Cookie付与とかもしんどかったので今回はクエリパラメータだけで済む署名付きURLにした
GitHub Actionsの記述
公式docsに従い、GitHubリポジトリに
/.github/workflows/
ディレクトリを用意してyamlで定義する。cf. https://docs.github.com/ja/actions/configuring-and-managing-workflows/configuring-a-workflow
動きとしてはこんな感じ
test_and_build_frontend
/frontend/
でlintして問題なければbuild- (テストがあればtestもやるべきだけど今回はテスト書いてない )
infra
- 前のjobが成功してから動くように
needs
で依存関係を作った/infra/aws-cdk/
でdiffして問題なければdeploy- masterブランチ以外へのpushなら、ステージング環境へのdiffまでやる
- developブランチへのpushなら、ステージング環境へのdiffとdeployまでやる
- masterブランチへのpushなら、本番環境へのdiffとdeployまでやる
ソースコード (長いので折りたたんだ)
/.github/workflows/cdk.ymlname: cdk on: push env: production-branch: 'refs/heads/master' staging-branch: 'refs/heads/develop' frontend-working-directory: frontend infra-working-directory: infra/aws-cdk aws-default-region: 'ap-northeast-1' jobs: test_and_build_frontend: runs-on: ubuntu-18.04 defaults: run: shell: bash working-directory: frontend timeout-minutes: 5 steps: - name: Checkout uses: actions/checkout@v2 - name: Cache frontend Node modules uses: actions/cache@v2 env: cache-name: frontend-cache-node-modules with: # npm cache files are stored in `~/.npm` on Linux/macOS path: ~/.npm key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- - name: Setup Node uses: actions/setup-node@v1 with: node-version: '12.18' - name: Setup dependencies run: npm ci - name: Lint run: npm run lint - name: Build run: npm run build - name: Upload frontend built path uses: actions/upload-artifact@v2 with: name: frontend-build-path path: frontend/dist infra: needs: [test_and_build_frontend] runs-on: ubuntu-18.04 defaults: run: shell: bash working-directory: infra/aws-cdk timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v2 - name: Cache infra Node modules uses: actions/cache@v2 env: cache-name: infra-cache-node-modules with: # npm cache files are stored in `~/.npm` on Linux/macOS path: ~/.npm key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- - name: Setup Node uses: actions/setup-node@v1 with: node-version: '12.18' - name: Setup dependencies run: npm ci - name: Download frontend built path uses: actions/download-artifact@v2 with: name: frontend-build-path path: frontend/dist - name: CDK Diff (Validate) with Stg if: ${{github.ref != env.production-branch }} run: npm run cdk diff -- --context target=stg env: AWS_DEFAULT_REGION: ${{ env.aws-default-region }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} V2OKIMOCHI_DEV_PROD_DOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_DOMAIN }} V2OKIMOCHI_DEV_PROD_SUBDOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_SUBDOMAIN }} V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID: ${{ secrets.V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID }} V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN: ${{ secrets.V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN }} V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID: ${{ secrets.V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID }} - name: CDK Diff (Validate) with Prod if: ${{github.ref == env.production-branch }} run: npm run cdk diff -- --context target=prod env: AWS_DEFAULT_REGION: ${{ env.aws-default-region }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} V2OKIMOCHI_DEV_PROD_DOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_DOMAIN }} V2OKIMOCHI_DEV_PROD_SUBDOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_SUBDOMAIN }} V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID: ${{ secrets.V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID }} V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN: ${{ secrets.V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN }} V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID: ${{ secrets.V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID }} - name: CDK Deploy to Stg if: ${{github.ref == env.staging-branch }} run: npm run cdk deploy -- --context target=stg --require-approval never env: AWS_DEFAULT_REGION: ${{ env.aws-default-region }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} V2OKIMOCHI_DEV_PROD_DOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_DOMAIN }} V2OKIMOCHI_DEV_PROD_SUBDOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_SUBDOMAIN }} V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID: ${{ secrets.V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID }} V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN: ${{ secrets.V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN }} V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID: ${{ secrets.V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID }} - name: CDK Deploy to Prod if: ${{github.ref == env.production-branch }} run: npm run cdk deploy -- --context target=prod --require-approval never env: AWS_DEFAULT_REGION: ${{ env.aws-default-region }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} V2OKIMOCHI_DEV_PROD_DOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_DOMAIN }} V2OKIMOCHI_DEV_PROD_SUBDOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_SUBDOMAIN }} V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID: ${{ secrets.V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID }} V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN: ${{ secrets.V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN }} V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID: ${{ secrets.V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID }}
npm run build
した成果物をupload保存しておき、後のjobでdownloadしてデプロイする- ブランチ名は
github.ref
で取得できるみたいnpm run
のコマンドに引数を渡す際は--
を間に差し込む
cf. https://docs.npmjs.com/cli/run-script- 今回cdkコマンドには引数として
target
を渡す (環境を判別する)
- 独自に引数を渡すには
--context
cdk deploy
には--require-approval never
も加える
- IAM roleなどセキュリティ関係の変更もスッと通す (デフォルトだと確認が差し込まれてしまい
y
でEnterせねばならない)- cdkのコードで利用する環境変数は、GitHub側のsecretsに定義した上でworkflow上の
env
に紐付けておく (今回はstepごとに定めた)timeout-minutes
を仕込んだ (cdkでたまに無限デプロイを始めてしまうので)おわり
pushすればこんな感じで動く (
Actions
タブから確認)
- 上の画像ではdevelopブランチがpushされたので、ステージング環境のdiffとdeployが行われている
- 対象外のstepはスキップされるらしい
- 途中で失敗すると、後のstepはスキップされる
- 画像右上の
Artifacts
はフロー内でアップロードしたfrontendのビルド成果物- 画像右上の
Re-run all jobs
でActionsを回し直せる
- 偶然失敗したケースならpushし直さなくて済む
- CircleCIと比べると、rerun対象のjobを選べない (全jobやり直してしまう)
かなり多くのサイトを参考にしたけど、あまりにもそれぞれのサイトから断片的な情報を組み合わせたので記憶の彼方に。。。
- 投稿日:2020-07-26T10:06:49+09:00
HTML5のフォームバリデーションをSPAの中で使う方法
最近、素のHTML5やCSS3をなるべく活用することに興味があります。
こういったベースの技術の方がフレームワークよりも息が長いので、一度学んだことを長期間役に立てることができるためです。※もちろんフレームワークも使うときは使います。例えばNuxt.jsは大好きです。
今回は、HTML5のフォームバリデーション機能を、Nuxt.jsで構築したSPAの中で使ってみたいと思います。
課題
HTML5のフォームバリデーションは、通常はformのsubmitを実行したタイミングで実施されます。
でも、SPAではformをsubmitしません。
どうしましょう...解決策
form.reportValidity() を使います。
これは、submitせずにバリデーションだけをかけるためのメソッドです。※2020年7月時点のMDNによれば、IE以外の全ての主要ブラウザでサポートされているようです。安心して使えますね。
https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reportValidityコード例
VInputItem.vue<template> <form> <label> 数量 </label> <input type="number" v-model="quantity" min="0" max="999" @chenge="onChengeInput" /> </form> </template> <script> export default { data() { return { unitprice: 0, quantity: 0, } }, methods: { onChangeInput(event){ this.$el.reportValidity() }, }, } </script>ポイント
この例では、直近のform要素を取得するために
$el
を使っています。
$elはVue.jsでルート要素を取得するためのプロパティです。
このサンプルコンポーネントでは、template直下のルート要素がformなので、それを一発で取得できています。※コンポーネント内の要素の構造によっては、$refsを用いて参照する方法もあると思います。
- 投稿日:2020-07-26T08:55:13+09:00
Vue(Nuxt.js)でスライドショーを自作する【ライブラリ使用無し】
下記の画像のようなスライドショーをVue(Nuxt.js)で自作する方法です。
Vue(Nuxt.js)でスライドショーを自作する
slide.vue<template> <div class="slide"> <div class="slide__wrapper"> <div v-show="index === page" v-for="(image, index) in images" :key="index" class="item" > <img :src="image" class="item__image" /> <span v-show="index === page" class="item__label">{{ index + 1 }}</span> </div> <div class="slide__right-btn" @click="nextPage"></div> <div class="slide__left-btn" @click="prevPage"></div> </div> </div> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ data() { return { page: 0, images: [ 'https://via.placeholder.com/500x300/0000FF/808080', 'https://via.placeholder.com/500x300/e6e6fa/808080', 'https://via.placeholder.com/500x300/ffb6c1/808080', 'https://via.placeholder.com/500x300/fff0f5/808080', 'https://via.placeholder.com/500x300/00ffff/808080', 'https://via.placeholder.com/500x300/ffd700/808080' ] } }, methods: { nextPage() { if (this.page + 1 >= this.images.length) { this.page = 0 return } this.page++ }, prevPage() { if (this.page <= 0) { return } this.page-- } } }) </script> <style lang="scss" scoped> .slide { padding-top: 200px; &__wrapper { position: relative; width: 500px; height: 300px; margin: 0 auto; } &__right-btn { position: absolute; right: 0; top: 0; width: 50%; height: 100%; z-index: 10; } &__left-btn { position: absolute; left: 0; top: 0; width: 50%; height: 100%; z-index: 10; } } .item { position: relative; &__label { position: absolute; top: 4px; left: 4px; background-color: #fff; width: 30px; height: 30px; line-height: 30px; border-radius: 50%; text-align: center; z-index: 999; } } </style>解説
v-for="(image, index) in images"dataのimagesをループして、スライドショーに使う画像を生成しています。
<div class="slide__right-btn" @click="nextPage"></div> <div class="slide__left-btn" @click="prevPage"></div>2つのエリアをdivで作り、右側をクリックすると次へ、左側を押すと前へ戻る仕様にしています。
v-show="index === page"v-showを使って一致する画像のみを表示させることで、スライドショーのように画像を切り替えることができます。
this.page++ this.page--クリックイベント毎にpageの値を変化させます。
if (this.page + 1 >= this.images.length) { this.page = 0 return }最後の画像になったら最初の画像に戻しています。
<span v-show="index === page" class="item__label">{{ index + 1 }}</span>v-forでは配列のindexも取得できるので、画像のラベルに取得したindexを使用しています。
- 投稿日:2020-07-26T08:38:54+09:00
Vue.jsで時計アプリを作ろう(後編:アプリの作成)
Vue.jsで時計アプリを作ろう(前編:開発環境の構築)の続きです。
ファイルの整理
まず、src/App.vueの中身を空にします。
src/App.vue<template> </template> <script> </script> <style> </style>次に、
- src/assets/logo.png
- src/components/HelloWorld.vue
を削除します。
最後にindex.htmlを編集します。
index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link href="https://fonts.googleapis.com/css?family=Roboto+Mono:700|Teko:600" rel="stylesheet"> <title>Vue Clock</title> </head> <body> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>コンポーネントの作成
src/componentsディレクトリにClock.vueを作成します。
Clock.vue<template> <div> <div class="container"> <p class="date">{{ year }}/{{ month }}/{{ day }}</p> <div class="time"> <p class="time-item hours">{{ hours }}</p> <p class="time-item minutes">{{ minutes }}</p> <p class="time-item seconds">{{ seconds }}</p> </div> </div> </div> </template> <script> const zeroPadding = (num, digit) => { return (Array(digit).join("0") + num).slice(-digit) } export default { data() { return { date: new Date(), } }, computed: { year() { return this.date.getFullYear() }, month() { return zeroPadding(this.date.getMonth() + 1, 2) }, day() { return zeroPadding(this.date.getDate(), 2) }, hours() { return zeroPadding(this.date.getHours(), 2) }, minutes() { return zeroPadding(this.date.getMinutes(), 2) }, seconds() { return zeroPadding(this.date.getSeconds(), 2) }, }, mounted() { this.setDate() setInterval(() => this.setDate(), 1000) }, methods: { setDate() { this.date = new Date() }, }, } </script> <style scoped> .container { background-color: #0e75bd; padding: 2%; } .date { text-align: right; color: #fff; font-family: 'Teko', sans-serif; font-size: 4rem; letter-spacing: .1em; margin: .0em 0; line-height: 1; } .time { display: flex; } .time-item { display: flex; justify-content: center; align-items: center; flex: 1 1; height: 100px; position: relative; z-index: 1; padding: 0.5em; margin: 3px; color: #fff; font-family: 'Roboto Mono', monospace; font-size: 3rem; line-height: 1; background-color: #33c7da; box-sizing: border-box; } .time-item:before { position: absolute; right: 5px; bottom: 1px; z-index: 1; color: #fff; font-family: 'Teko', sans-serif; font-size: 1.4rem; letter-spacing: .05em; } .hours:before { content: "Hours"; } .minutes:before { content: "Minutes"; } .seconds:before { content: "Seconds"; } </style>次に、src/App.vueにコードを追加します。
src/App.vue<template> <div> <Clock class="clock"/> </div> </template> <script> import Clock from "@/components/Clock" export default { components: { Clock, }, } </script> <style scoped> .clock { width: 80%; max-width: 500px; margin: 30px auto; } </style> <style> html { font-size: 62.5%; } body { margin: 0; } p { margin: 0; } </style>ローカルサーバを起動します。(すでに起動している場合、ファイルを保存すればブラウザが自動更新されます)
$ npm run devブラウザを確認します。
上の画面のように表示されれば、時計アプリは完成です。
ソースコードはこちらで確認できます。
- 投稿日:2020-07-26T01:01:44+09:00
【Vue.js】CompositionAPIでTODOリスト
もうすぐVue3が出ると聞いたので、目玉機能のひとつ「CompositionAPI」を試してみました。
題材は定番のTODOリスト(の超簡易版)です。
CompositionAPIとは
a set of additive, function-based APIs that allow flexible composition of component logic.
(公式より引用)
- Vue3で正式リリース予定の便利機能
- data, method, computed, ライフサイクルメソッドなどを関数として定義できる
- よってTypeScriptで型付けしやすい
- thisを使う必要がなくなる
- 機能Aのdata, method, computed => 機能Bのdata, computed ...のように、関心の単位で処理をまとめられる
- 関数をexport/importして、コンポーネント間でロジックを再利用できる
- 柔軟な構成(Composition)でコードを配置できることが名前の由来
- 要するに、VueにおけるReact Hooks的なもの
(公式にもReact Hooksから着想を得て作られた と書いてある)- 2系でも使える(
npm install @vue/composition-api
すればOK)
- 従来のOptionsAPI(data, method, computed)と併用可能 (多少の制限はある)
- Vue3系になっても、OptionsAPIとCompositionAPIは共存していく方針とのこと
メリット
デメリット
- 柔軟な書き方ができる分、好き放題できてしまえる
(コード品質の上限を引き上げるが、下限も引き下げる。公式では、上限を引き上げるメリットのほうが大きいと言っている)- 現状だとベストプラクティス的な書き方がなく、コード品質が書き手にかなり依存する
...というわけで、以下は試してみたコードです。Vue2系でCompositionAPIを使えるようにする
参考: https://github.com/vuejs/composition-api
インストール
$ npm install -D @vue/composition-apiVue.use()する
main.tsimport VueCompositionApi from '@vue/composition-api'; Vue.use(VueCompositionApi);
超簡易版のTODOリストを作成
Todos.vue<template> <div class="container"> <h2>TODO LIST</h2> <form class="todo-form"> <span>何をやる?</span> <input type="text" v-model="state.nameForSubmit" /> <span>いつまでにやる?</span> <input type="date" v-model="state.duedateForSubmit" /> <button @click="addTodo">リストに追加</button> </form> <div class="todo-list"> <input type="checkbox" @change="toggleShouldExtractIncompleteTodos" /> 未完了のTODOだけ表示 <div v-for="todo in todosToShow" :key="todo.id"> <div class="todo-list-item"> {{ 'id(' + todo.id + '), name(' + todo.name + '), duedate(' + todo.duedate + '), isDone(' + (todo.isDone === true ? '完了' : '未完了') + ')' }} </div> <button @click="toggleIsDone(todo.id)"> {{ todo.isDone === true ? '未完了にする' : '完了させる' }} </button> </div> </div> </div> </template> <script lang="ts"> import { reactive, computed, defineComponent } from '@vue/composition-api'; interface TODO { id: number; name: string; duedate: number; isDone: boolean; } // defineComponent()を使うことで、コンポーネント化できる (使わずに、共通ロジックを定義することもできる) export default defineComponent({ // setup()は読み込み時に1度だけ実行される ←Hooksとの相違点 setup() { // 従来のdataに相当する部分 // * ひとつの変数に全部突っ込むこともできるが、せっかくなのでtemplateへ共有する状態、しない状態を分けてみた // * reactive()を使うことでリアクティブになる // * 似たような用途の関数にref()があるが、今回の範囲では使わなかった const privateState = reactive({ todos: [] as TODO[], idForSubmit: 1, }); const publicState = reactive({ nameForSubmit: '', duedateForSubmit: 0, shouldExtractIncompleteTodos: false, }); // 従来のmethodに相当する部分 // * もちろんstateには自由にアクセスできる // * "this"を使っていない点に注目 const addTodo = (): void => { privateState.todos.push({ id: privateState.idForSubmit++, name: publicState.nameForSubmit, duedate: publicState.duedateForSubmit, isDone: false, }); }; const toggleIsDone = (id: number): void => { privateState.todos.forEach((todo) => { if (todo.id === id) { todo.isDone = !todo.isDone; } }); }; const toggleShouldExtractIncompleteTodos = (): void => { publicState.shouldExtractIncompleteTodos = !publicState.shouldExtractIncompleteTodos; }; // 従来のcomputedに相当する部分 // * reactive()を使わないとリアクティブにならないので注意 // * 一応、const todosToShow = computed(...); と書くこともできる。リアクティブにしたくないときに使えるのかも? const todosToShow = reactive( computed((): TODO[] => { if (publicState.shouldExtractIncompleteTodos === true) { return privateState.todos.filter((todo) => { return todo.isDone === false; }); } return privateState.todos; }) ); // setup()の最後に色々返す。ここで返したものはtemplateから参照できる。 return { state: publicState, addTodo, toggleIsDone, toggleShouldExtractIncompleteTodos, todosToShow, }; }, }); </script> <style lang="scss" scoped> .container { border: solid 1px; margin: 50px; padding: 8px; } .todo-form { input { border: solid 1px; border-radius: 4px; display: block; margin-bottom: 8px; width: 200px; } button { background-color: aliceblue; border: solid 1px; border-radius: 4px; margin-right: 8px; margin-top: 8px; width: 200px; } } .todo-list { margin-top: 16px; .todo-list-item { display: inline-block; width: 500px; } button { background-color: aliceblue; border: solid 1px; border-radius: 4px; width: 200px; } } </style>
おまけ: コンポーネント間でpropsを渡す
Parent.vue<template> <div class="parent-component"> <h2>Parent Component</h2> count <div>{{ state.count }}</div> count * 2 <div>{{ state.double }}</div> <br> <button @click="increment" class="button"> increment(parent) </button> <br> <ChildComponent :count="state.count" :increment="increment" /> </div> </template> <script lang="ts"> import { reactive, computed, onMounted, defineComponent } from "@vue/composition-api"; import ChildComponent from "@/containers/ChildComponent.vue" export default defineComponent({ components: { ChildComponent }, setup() { // setup(): top => setup(): bottom => onMounted() の順で呼び出される console.log('setup(): top'); onMounted(() => { console.log('onMounted()') }) const state = reactive({ count: 1, double: computed((): number => { return state.count * 2; }), }) const increment = (): void => { state.count++; } console.log('setup(): bottom'); return { state, increment, }; } }); </script> <style lang="scss" scoped> .parent-component { margin-left: 100px; border: solid 1px; padding: 5px; } .button { border: solid 1px; background-color: aliceblue; border-radius: 4px; width: 200px; } </style>Child.vue<template> <div class="child-component"> <h2>Child Component</h2> count * count <div>{{ state.power }}</div> <br> <button @click="increment" class="button"> increment(child) </button> </div> </template> <script lang="ts"> import { reactive, computed, defineComponent } from "@vue/composition-api"; type Props = { count: number; increment: () => void; }; export default defineComponent({ props: { count: { type: Number, required: true, }, increment: { type: Function, required: true, } }, setup(props: Props) { const state = reactive({ power: computed((): number => { return props.count * props.count; }), }) return { state, }; } }); </script> <style lang="scss" scoped> .child-component { border: solid 1px; padding: 5px; margin-top: 20px; } .button { border: solid 1px; background-color: aliceblue; border-radius: 4px; width: 200px; } </style>
やってみた感想
- 形式は違えど、Vueだけあってかなり直感的に書ける
- 普段OptionsAPIでやってるのと同様のことができそうという感覚が得られた
- 関数で書けるのはやっぱり便利
- 確かに柔軟に書ける
- けど無法地帯になりうるのも確かで、ベストプラクティスが出回るまでは無理に導入しなくてもいいかなという印象
- reactive()とref()の使い分けがまだピンと来ていない
- 公式曰く「CompositionAPIを効果的に使うには、両者を使い分けなければならない」らしい
- とりあえず超簡易TODOリスト程度の範囲では、reactive()だけで困ることがなかった
- この辺は追々学んでいこう
以上です。
Vue3が順調にリリースされますように。
- 投稿日:2020-07-26T01:00:10+09:00
element-uiで特定のコンポーネントが表示されない
どういうわけかrails webpackerではel-table-columnが表示されなかったのですが、ElementUIのバージョンを下げたら動くこと確認。
とはいえ、最新版のElementUIじゃないと動かないコンポーネントもあるので、yarnで複数のバージョンをインストールする荒業でしのぎました。
yarn remove element-ui;した後に
yarn add element-uiこれで最新版がインストールされる
んで、ダウングレード版はyarn add element-ui-2.6.3@npm:element-ui@2.6.3でインストールできた。
使うときはimport ElementUI from 'element-ui-2.6.3'こんな感じ。
8時間くらいハマりました。