- 投稿日:2019-06-27T23:45:50+09:00
Vue・VueCLIのバージョンが分からん!
- 投稿日:2019-06-27T19:55:13+09:00
はじめてのvue-property-decorator
この記事はVueを勉強している段階からTypeScriptでクラスベースのVueアプリを作りたい!という方へ向けて例を交えながら
vue-property-decorator
の機能を基本と応用、上級の3セクションに分けて説明していきます。基本では、Vueでアプリを作る上で必須となる機能を、応用では用意されている便利なデコレータを、上級では普通の開発ではほぼ使わない機能について説明します。
とりあえずは基本のみ理解しておけば困ることはないでしょう。動作確認バージョン
基本
コンポーネントの定義
@Component
は続けて定義しているクラスをVueが認識できる形式に変換しています。以下の2つは同じ意味です。
<script> export default {}; </script><script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class SampleComponent extends Vue {} </script>この時、
vue-property-decorator
のVue
クラスを継承することを忘れないように気をつけてください。Data
Dataはクラスのメンバーとして定義するだけで利用できます。
以下のサンプルでは名前と年齢をDataに持たせています。
<script> export default { data() { return { name: 'simochee', age: 21 } } }; </script><script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class SampleComponent extends Vue { name = 'simochee'; age = 21; } </script>Dataをテンプレート内で使用するときはプレーンなVueと同じように参照できます。
<template> <!-- simochee (21) --> <p>{{name}} ({{age}})</p> </template>Computed
算出プロパティ(Computed)はクラスのGetterとして定義することで利用できます。
以下のサンプルではDataに定義されたスコアを3倍する算出プロパティを定義しています。
<script> export default { data() { return { score: 55 } }, computed: { triple() { return this.score * 3; } } }; </script><script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class SampleComponent extends Vue { score = 55; get triple() { return this.score * 3; } } </script>Computedをテンプレート内で使用するときはプレーンなVueと同じように参照できます。
<template> <!-- Triple score: 163! --> <p>Triple score: {{triple}}!</p> </template>メソッド
メソッドはクラスのメソッドとして定義するだけで利用することができます。
以下の例では、ボタンが押されたときに
onClickButton
メソッドを呼び出しています。<template> <button @click="onClickButton">Click Me!</button> </template>このようなテンプレートがあったときの
onClickButton
は以下のように定義できます。<script> export deafult { methods: { onClickButton() { // ボタンが押されたときの処理 } } }; </script><script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class SampleComponent extends Vue { onClickButton() { // ボタンが押されたときの処理 } } </script>Reactのようにメソッドに
this
をバインドする必要はありません。ライフサイクルフック
ライフサイクルフックは、クラスにライフサイクルの名前でメソッドを定義するだけで利用できます。
<script> export default { mounted() { // コンポーネントがマウントされたときの処理 }, beforeDestroy() { // コンポーネントが破棄される直前の処理 } } </script><script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class SampleComponent extends Vue { mounted() { // コンポーネントがマウントされたときの処理 } beforeDestroy() { // コンポーネントが破棄される直前の処理 } } </script>
vue-property-decorator
ではライフサイクルフックとメソッドが同じ領域で定義されるため、ライフサイクルの名前でメソッドを定義しないように注意が必要です。
@Component
@Component
は引数としてVueのオブジェクトを指定することができます。以降で各種デコレータを紹介しますが、そこで定義できない
components
やfilters
、mixins
などのプロパティは@Component
の引数として指定します。<script> export deafult { components: { AppButton, ProductList }, directives: { resize } filters: { dateFormat }, mixins: [ PageMixin ] }; </script><script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component({ components: { AppButton, ProductList }, directives: { resize } filters: { dateFormat }, mixins: [ PageMixin ] }) export default class SampleComponent extends Vue { } </script>他にも、以下のドキュメントで紹介されているプロパティを指定できます。
@Prop
@Prop
は続けて定義したメンバーをprops
として使用できるようにします。親コンポーネントからは定義したメンバー名で
props
を指定できます。<script> export deafult { props: { userName: { type: String, required: true }, isVisible: { type: Boolean, default: false } } }; </script><script lang="ts"> import { Component, Prop, Vue } from 'vue-property-decorator'; @Component export default class SampleComponent extends Vue { @Prop({ type: String, required: true }) userName: string; @Prop({ type: Boolean, defualt: false }) isVisible: boolean; } </script>
@Prop
の引数にはprops
で指定可能なオプションがすべて指定できます。
@Watch
@Watch
は第1引数に監視したい値へのパスを、第2引数にウォッチャのオプションを指定できます。以下のサンプルでは単一のDataとObjectのプロパティの値を監視するメソッドを定義しています。
なお、
immediate: true
はコンポーネント初期化時にも処理を実行するかを指定するオプションです。<script> export deafult { data() { isLoading: false, profile: { name: 'simochee', age: 21 } }, watch: { isLoading() { // ローディング状態が切り替わったときの処理 }, 'profile.age': { handler: function() { // プロフィールの年齢が変更されたときの処理 }, immediate: true } } }; </script><script lang="ts"> import { Component, Watch, Vue } from 'vue-property-decorator'; @Component export default class SampleComponent extends Vue { isLoading = false; profile = { name: 'simochee', age: 21 }; @Watch('isLoading') onChangeLoadingStatus() { // ローディング状態が切り替わったときの処理 } @Watch('profile.age', { immediate: true }) onChangeProfileAge() { // プロフィールの年齢が変更されたときの処理 } } </script>Vueの仕様からも分かる通り、
@Watch
は同じパスに対して複数回指定することはできません。
複数回指定された場合は後勝ちとなるため、先に定義したウォッチャは実行されません。応用
@SyncProp
Vue.jsでは
props
を指定する際に.sync
修飾子を付与することで、子コンポーネントから親コンポーネントの値を変更することができるようになります。仕組みとしては、
@update:<Prop名>
というイベントを受け取ったらDataに代入するという処理を暗黙的に行っています。// 親コンポーネント <template> <!-- 以下の2つは同じ意味 --> <ChildComponent :childValue.sync="value" /> <ChildComponent :childValue="value" @update:childValue="value = $event" /> </template>このとき、子コンポーネント以降で
.sync
プロパティをバケツリレーする際に便利なのが@PropSync
デコレータです。このデコレータを使用しない場合は、以下のように書かなければいけませんでした。
// 子コンポーネント <script lang="ts"> import { Component, Prop, Vue } from 'vue-property-decorator'; @Component export default class SampleComponent extends Vue { @Prop({ type: String }) childValue: string; // value を変更したいときに呼び出す updateValue(newValue) { this.$emit('update:childValue', newValue); } } </script>これが、
@PropSync
で定義した場合はメンバーへ値を代入するだけで同等の処理を実現することができます。<script lang="ts"> import { Component, PropSync, Vue } from 'vue-property-decorator'; @Component export default class SampleComponent extends Vue { @PropSync({ type: String }) childValue: string; // value を変更したいときに呼び出す updateValue(newValue) { this.childValue = newValue } } </script>代入するだけで値の変更を通知できるため、さらに
.sync
で孫コンポーネントへ値を渡す場合なども、とてもシンプルに書くことができます。<template> <SunComponent :sunValue.sync="childValue" /> </template>
@Emit
Vueではコンポーネント間での値を双方向にやり取りすることができます。
親から子への値渡しはPropを指定することで行い、子から親への値渡しはイベントを通知することによってアクションや値を受け渡せるようになっています。
このとき、子から親へ値を渡すときのイベントを発行するのが
$emit
メソッドです。以下のサンプルでは子コンポーネントで送信処理が行われたこと
submit
イベントとして親コンポーネントへ通知し、受け取った値を元に親コンポーネント側でリクエストを送信しています。// 子コンポーネント <template> <form @submit="onSubmit"> <input v-model="value"> <button type="submit">Submit</button> </submit> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class ChildComponent extends Vue { value = ''; // 値を送信する処理 onSubmit() { this.$emit('submit', this.value); } } </script>// 親コンポーネント <template> <ChildComponent @submit="onReceiveSubmit" /> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; import ChildComponent from './ChildComponent.vue'; @Component({ components: { ChildComponent } }) export default class ParentComponent extends Vue { async onReceiveSubmit(newValue: string) { // $emitでの第2引数を受け取ることができる await this.$request.post(newValue); } } </script>
@Emit
では$emit
の処理を事前に定義することができます。イベント名は
@Emit
の第1引数に明示的に指定するか、省略した場合は続けて定義しているメソッド名が利用されます。また、メソッドで値を返却すれば
$emit
でその値を送ることもできます。上記のサンプルの子コンポーネントを
@Emit
で書き換えると以下のようになります。<template> <form @submit="submit"> <input v-model="value"> <button type="submit">Submit</button> </submit> </template> <script lang="ts"> import { Component, Emit, Vue } from 'vue-property-decorator'; @Component export default class ChildComponent extends Vue { value = ''; // 値を送信する処理 // イベント名を指定しない場合でも () は省略できない @Emit() submit() { return this.value; } } </script>この他に、
@Emit
を非同期メソッドへ設定することもできます。なお、キャメルケースでイベント名、メソッド名を指定した場合、親コンポーネントで受け取る際にはケバブケースへ変換されますので注意が必要です。
// 子コンポーネント @Emit() submitForm() {} // 親コンポーネント <ChildComponent @submit-form="onSubmit" @submitForm"onSubmit" // 発火しない />
@Ref
@Ref
は$refs
で参照できる要素、コンポーネントの型を定義します。
事前に定義しておくことでタイポや変更へ対応しやすくなります。<template> <ChildComponent ref="childComponent" /> <button ref="submitButton">Submit</button> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; import ChildComponent from '@/component/ChildComponent.vue'; @Component({ components: { ChildComponent } }); export default class SampleComponent extends Vue { @Ref() childComponent: ChildComponent; @Ref() submitButton: HTMLButtonElement; mounted() { // 子コンポーネントのメソッドを実行する this.childComponent.updateValue(); // ボタンをフォーカスする this.submitButton.focus(); } } </script>上級
以降は上級者向けのため、あまり詳しく記載しません。必要であれば公式ドキュメントなどを参照してください。
@Model
VueのModelを定義します。VueではModelを指定する際にPropも定義しそちらに型情報などを記載しなければいけませんでしたが、デコレータでは
@Model
に併せて定義できます。以下の2つは同じ意味です。
<script> export deafult { props: { value: { type: String, required: true } }, model: { prop: 'value', event: 'update' } }; </script><script lang="ts"> import { Component, Model, Vue } from 'vue-property-decorator'; @Component export default class SampleComponent extends Vue { @Model('update', { type: String, required: true }) value: string; } </script>このとき、暗黙的に
value
がPropとして定義されるためvalue
というdata
やmethods
は定義できません。
@Provide
/@Inject
Vueでは親で
provide
として定義した値をその子要素(親子でなくても良い)からinject
で参照することができます。以下の2つは同じ意味です。
<!-- Parent.vue --> <script> export deafult { provide: { foo: 'foo', bar: 'bar' } }; </script> <!-- Child.vue --> <script> export deafult { inject: { foo: 'foo', bar: 'bar', optional: { from: 'optional', default: 'default' } } }; </script><!-- Parent.vue --> <script lang="ts"> import { Component, Provide, Vue } from 'vue-property-decorator'; @Component export default class ParentComponent extends Vue { @Provide() foo = 'foo'; @Provide('bar') baz = 'bar'; } </script> <!-- Child.vue --> <script lang="ts"> import { Component, Inject, Vue } from 'vue-property-decorator'; @Component export default class ChildComponent extends Vue { @Inject() foo: string; @Inject('bar') bar: string; @Inject({ from: 'optional', default: 'default' }) optional: string; @Inject(symbol) baz: string; } </script>
@ProvideReactive
/@ProvideInject
@Provide
/@Inject
の拡張です。親コンポーネントから@ProvideReactive
として提供された値の変更を子コンポーネントでキャッチできるようになります。<!-- Parent.vue --> <script lang="ts"> import { Component, ProvideReactive, Vue } from 'vue-property-decorator'; @Component export default class ParentComponent extends Vue { @ProvideReactive() foo = 'foo'; } </script> <!-- Child.vue --> <script lang="ts"> import { Component, InjectReactive, Vue } from 'vue-property-decorator'; @Component export default class ChildComponent extends Vue { @InjectReactive() foo: string; } </script>
readonly
と!
、?
について
vue-property-decorator
のサンプルコードではreadonly
やprop!: String
のような!
が登場しています。これらはいずれもTypeScriptの機能です。
readonly
修飾子は、メンバー変数を書き込み専用とするためのものです。
VueではProp、Modelへの直接の代入はエラーとなります。誤った代入を事前に防ぐために
@Prop
および@Watch
で定義したメンバー変数へはreadonly
修飾子を付与することをおすすめします。<script lang="ts"> import { Component, Prop, PropSync, Watch, Vue } from 'vue-property-decorator'; @Component export default class SampleComponent extends Vue { @Prop({ type: String }) readonly name: string; @Watch('update', { type: Object }) readonly profile: IProfile; @PropSync({ type: String }) value: string; // 代入可能 } </script>また、デコレータで定義されたすべてのメンバー変数についている
!
は NonNullAssertionオペレータ と呼ばれる機能です。
!
がついたプロパティがNull/Undefinedではないことを明示します。ただし、
!
はrequired: true
またはデフォルト値が設定されているプロパティにのみ指定することをおすすめします。
逆に、必須項目でなくデフォルト値も指定されていない場合は、?
を指定することをおすすめします。この
?
はプロパティが任意項目であり、undefined
の可能性があることを明示します。<script lang="ts"> import { Component, Prop, Vue } from 'vue-property-decorator'; @Component export default class SampleComponent extends Vue { @Prop({ type: String, required: true }) readonly name!: string; @Prop({ type: Array, default: () => [] }) readonly items!: string[]; @Prop({ type: Object }); readonly profile?: IProfile; mounted() { // undefinedの可能性のあるオブジェクトのプロパティを // 参照しようとしたのでタイプエラーになる profile.age; } } </script>より強固に型安全にしたい場合はこれらに気をつけて開発するようにしてみてください。
- 投稿日:2019-06-27T19:45:44+09:00
vue-simple-suggestで簡単にテキスト入力候補をリストで表示
はじめに
正確な名前が分からなかったり、検索項目が多い場合サジェストは非常にUXの質を高めてくれます。
今回はTypescriptとVueで簡単にサジェストの表示を実装したのでその記録を残しておきます。
今回利用するのがこちらです。
vue-simple-suggestのインストール
まずはvue-simple-suggestをインストールしましょう。
yarn add vue-simple-suggestこれでインストール完了しました。
これだけだとインポートしたときに方エラーが出てしまいます。型の定義ファイルにvue-simple-suggestを宣言しておきましょう
shims-vue.d.tsdeclare module '*.vue' { import Vue from 'vue'; export default Vue; } declare module 'vue-simple-suggest';実装
最終的なコードは以下のようになります。
<template> <vue-simple-suggest v-model="chosen" :list="simpleSuggestionList" :filter-by-query="true" :styles="autoCompleteStyle" display-attribute="firstName" > <template slot="misc-item-above"> <h5>ユーザー一覧</h5> <tr> <th>ファーストネーム</th> <th>ラストネーム</th> </tr> </template> <tr slot="suggestion-item" slot-scope="{ suggestion }"> <td>{{ suggestion.firstName }}</td> < td>{{ suggestion.lastName }}</td> </tr> </vue-simple-suggest> </template> <script lang="ts"> import { Component, Vue, Prop } from "vue-property-decorator"; import VueSimpleSuggest from "vue-simple-suggest"; import Button from "@/components/Atoms/Button.vue"; import "vue-simple-suggest/dist/styles.css"; @Component({ name: "FormWIthSearch", components: { Button, VueSimpleSuggest } }) export default class FormWIthSearch extends Vue { private chosen: string = ""; private autoCompleteStyle: {} = { vueSimpleSuggest: "position-relative", inputWrapper: "", defaultInput: "form-control", suggestions: "position-absolute list-group", suggestItem: "list-group-item" }; public simpleSuggestionList() { return [ { id: 1, firstName: "タロウ", lastName: "サトウ" }, { id: 2, firstName: "ジロウ", lastName: "イノウエ" }, { id: 3, firstName: "サブロウ", lastName: "タナカ" } ]; } } </script>順番に説明します。
ボタンは別コンポーネントから読み込んでいますが普通のボタンと思っていただいて構わないです。
CSSにはBootstrapを利用しています。
各プロパティの説明です。
chosenには選択したオプションの値が入ります。今回はfirstNameを指定しています。またこの値から検索で絞り込みます。
続いてautoCompleteStyleです。テキストボックスやサジェストの下に表示されるリストのクラスを付与することができます。
CSSは通常通りお好みのやり方であてていただければ大丈夫です。
simpleSuggestionListでリストの配列を返して検索リストとして割り当てます。
これは一点注意でデフォルトの状態ではidを付けなければエラーがでます。
また私の場合はオブジェクトのfirstNameを検索対象としています。display-atributeを指定しないとデフォルトでは配列に文字列を入れなければなりません。
さらにvue-simple-suggest内でslot="misc-item-above"をタグにつけるとheaderを付けたりできます。
他にもオプションはまだまだたくさんあります。詳しくはgihubのREADMEを読んでみましょう。
まとめ
今回はVueによるサジェストの実装をしました。UXを向上させるには必須ので機能です。
このプラグインを活用して爆速でサジェストを実装しましょう。
- 投稿日:2019-06-27T18:19:24+09:00
Jestの結果がおかしくなったら--clearCacheしよう。
ある日の出来事...
.vueでレイアウトちょっと変えただけだけどいちおうテストしとこうっと。
やぁ!
yarn run test:unit
結果はどうかな?
...なんかめちゃめちゃやんけ!!
Uncovered Lineの表示もソースコードと全然合っていません。
こんな時は慌てず騒がず(騒いだけど...)キャッシュをクリアしましょう。
うりゃ〜!
yarn run test:unit --clearCache $ vue-cli-service test:unit --clearCache Cleared /var/folders/2l/gq85xz3j06x5y3rzsmlnq8vr00172j/T/jest_uvlみたいになります。
再びテストします!やぁ!!
yarn run test:unit
ばっちりですね。
- 投稿日:2019-06-27T17:18:37+09:00
JavaScriptとElmを比べてみた〜後編・Vue.jsとも比べてみた〜
※前編はこちらやで。
ハスケル子「引き続き、JavaScriptとElm・・・」
ハスケル子「そしてVue.jsもちょこっと比べてみましょう」オブジェクト(のようなもの)
JavaScriptのオブジェクト
const takashi = { displayName: "たかし", age: 36, height: 173, weight: 73 };ワイ「
displayName
とかage
とかが、プロパティいうやつやな」
ワイ「takashi.age
とかすると、プロパティの値にアクセスできんねん」Elmのレコード
takashi = { displayName = "たかし" , age = 36 , height = 173 , weight = 73 }ワイ「Elmではレコードいうねんな」
ワイ「JSのオブジェクトとけっこう似てるな」
ワイ「displayName
とかage
とかのことは」
ワイ「フィールドっていうんやな」
ワイ「JSと同じくtakashi.age
って書けばフィールドの値にアクセスできんねん」オブジェクトのプロパティを更新
JavaScriptのオブジェクト
takashi.age = 37; takashi.weight = 83;ワイ「
const
で宣言した再代入不可なオブジェクトでも、プロパティは変更できてまうんやな」Elmのレコード
newTakashi = { takashi | age = 37, weight = 83 }ワイ「Elmでは全ての値が不変やから
takashi.age = 37
みたいな上書きはできひんねん」
ワイ「せやから、1つ歳をとって10Kg太ったnewTakashi
という新しいレコードを作る形になる」
ワイ「元々のtakashi
は36歳のまま、別に存在すんねん」ドキュメントの書き方
JavaScript (JSDoc)
/** * @param {Number} num * @param {String} str * @return {String} */ function displayNumber (num, str) { return num + str; }ワイ「関数の使い方が分かりやすいように」
ワイ「引数や戻り値の型をコメントで書いたりすんねんな」
ワイ「まぁ、実装とズレてても動くから、努力目標やけどな・・・」Elmの場合は型で表現
displayNumber : Int -> String -> String displayNumber num str = String.fromInt num ++ strワイ「
displayNumber : Int -> String -> String
いうのは」
ワイ「このdisplayNumber
いう関数は」
ワイ「引数としてInt型の値とString型の値を受け取って」
ワイ「戻り値としてString型の値を返す」
ワイ「そんな関数ですよ〜っていう」
ワイ「型注釈いうやつや」ワイ「しかも、この型注釈で書いた通りに実装せえへんと」
ワイ「ちゃんとコンパイルエラーが出て教えてくれんねん」
ワイ「TYPE MISMATCH(型の不一致)です〜、言うてな」ワイ「つまり、強制力のある注釈や!」
ビューの書き方
Vue.jsの場合(単一ファイルコンポーネント)
<template> <div class="container"> <button>増やす</button> <input type="text"> <button>減らす</button> </div> </template>ワイ「ほぼhtmlやな」
ワイ「読みやすいな」Elmの場合
view model = div [ class "container" ] [ button [] [ text "増やす" ] , input [ type_ "text" ] [] , button [] [ text "減らす" ] ]ワイ「この
div
とかbutton
ていうのがタグ名やね・・・?」ハスケル子「まあそうなんですけど」
ハスケル子「タグ名というより、れっきとしたElmの関数です」
ハスケル子「関数なので───」joinButton = button [] [ text "参加する" ]ハスケル子「───こんな感じで変数に格納すれば」
ハスケル子「それだけでコンポーネントみたいに使えますし」commonButton buttonText = button [] [ text buttonText ]ハスケル子「↑こう、引数としてテキストを受け取る関数にすれば」
ハスケル子「propsを受け取って表示するタイプのコンポーネントもサクッと作れます」ワイ「おお」
ワイ「コンポーネントも関数そのものやから、コードの中で自然に使えるな」ハスケル子「そうなんです」
ハスケル子「リスト1の分だけ回してli
要素を生成したい、なんて場合も簡単です」イベントリスナ登録(のようなもの)
Vue.jsの場合
<button @click="incrementFunc">増やす</button> <input type="text"> <button @click="decrementFunc">減らす</button>ワイ「見たままやな」
ワイ「ボタンをクリックするとincrementFunc
かdecrementFunc
という」
ワイ「関数が実行されんねんな」
ワイ「関数はmethods
の中に書いとけばええんや」Elmの場合
button [ onClick Increment ] [ text "増やす" ] , input [ type "text" ] [] , button [ onClick Decrement ] [ text "減らす" ]ワイ「こう書いておけば、このボタンをクリックした時に・・・?」
ハスケル子「
Increment
またはDecrement
というメッセージが生み出されます」ワイ「メッセージ・・・?」
ワイ「そのメッセージはどこで受け取るん?」ハスケル子「状態の更新内容を定義する
update
っていう関数で受け取ります」update msg model = case msg of Increment -> { model | int = model.int + 1 } Decrement -> { model | int = model.int - 1 }ハスケル子「
Increment
というメッセージが来たら」
ハスケル子「model
・・・つまりVuexでいうstore
のstate
みたいなもんですね」
ハスケル子「要はmodel
イコール状態です」
ハスケル子「そのmodel
の中のint
を1
増加させます」
ハスケル子「メッセージがDecrement
だった場合は1
減らす感じですね」
ハスケル子「そして、それによって生成された新しいmodelを戻り値として返すって感じです」ワイ「ほえ〜、新しいmodel、つまり新しい状態を返すと」
ワイ「それがリアクティブにビューに反映されるっていうこと?」ハスケル子「そうです」
ワイ「そうなんやね〜」
ワイ「Elmって、VueとかReactみたいに仮想DOMを内蔵してたんやね」
ワイ「あとVuexやRedux相当の機能もか」ハスケル子「そうです」
ハスケル子「っていうかVuexもReduxも、Elmの影響を受けてます」
ハスケル子「Elmというか、今みたいなThe Elm Architectureというパターンの影響ですね」
ハスケル子「しかもElmはとってもシンプルなので」
ハスケル子「学習コストが低くて、やめ太郎さんにピッタリです」ワイ「どういう意味やねん」
ハスケル子「オンラインエディタでさっきのカウンタのサンプルコードを色々いじってみると」
ハスケル子「更に分かると思いますよ」ワイ「やってみるわ」
ワイ「おおきにやで、ハスケル子ちゃん」ハスケル子「Vue勉強しててもReact勉強してても」
ハスケル子「副作用を起こさないように、とか」
ハスケル子「外部の状態に依存しない純粋な関数・・・つまり参照透過的な関数を書こう、とか」
ハスケル子「色んなドキュメントに書いてあるんですよ」
ハスケル子「じゃあ、そういう風にしか書けないElmやりゃあいいんですよ」ワイ「お、おう・・・」
ワイ「前向きに検討するわ・・・」
ワイ「っていうか、今回JSやVueと比較したおかげでElmの文法がかなり分かったから」
ワイ「普通に読み書きできそうやな・・・」
ワイ「やってみるで!」〜おしまい〜
配列みたいなやつやで。 ↩
- 投稿日:2019-06-27T16:21:22+09:00
既存のVue.jsのプロジェクトにJestでのテスト環境を構築する
TL;DR
- VueのプロジェクトにJestによる環境を構築するためにやったことをまとめた
- Jestについては説明しない
ディレクトリ構成
├── jest.config.js ├── package.json ├── src │ ├── js │ │ ├── components │ │ └── main.js │ └── sass ├── tests │ └── unit └── webpack.config.jsパッケージのインストール
とりあえず基本的なものをインストールします。
$ npm i -D @vue/test-utils jest vue-jest babel-jest
package.json
にテストを走らせるためのタスクを追加します。package.json"scripts": { "test:unit": "jest" }Jestの設定をする
jest.config.jsmodule.exports = { moduleFileExtensions: ["js", "jsx", "json", "vue"], transform: { "^.+\\.vue$": "vue-jest", "^.+\\.jsx?$": "babel-jest" }, transformIgnorePatterns: ["node_modules/"], moduleNameMapper: { "^@/(.*)$": "<rootDir>/src/js/$1" }, testMatch: [ "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" ] };試してみる
$ npm run test:unit > vue.build@1.0.0 test:unit /Users/xxx/project/vue-jest-test > jest PASS tests/unit/example.spec.js HelloWorld.vue ✓ renders props.msg when passed (15ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.206s, estimated 5s Ran all test suites.スナップショットテストを導入する
$ npm i -D jest-serializer-vuejest.config.js
jest.config.js
に以下を追加する。module.exports = { ... snapshotSerializers: ["jest-serializer-vue"] ... };追加後の
jest.config.js
module.exports = { moduleFileExtensions: ["js", "jsx", "json", "vue"], transform: { "^.+\\.vue$": "vue-jest", "^.+\\.jsx?$": "babel-jest" }, transformIgnorePatterns: ["node_modules/"], moduleNameMapper: { "^@/(.*)$": "<rootDir>/src/js/$1" }, snapshotSerializers: ["jest-serializer-vue"], testMatch: [ "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" ] };結果
$ npm run test:unit > vue.build@1.0.0 test:unit /Users/xxx/project/vue-jest-test > jest PASS tests/unit/SampleComp.spec.js SampleComp.vue ✓ renders props.text when passed (16ms) › 1 snapshot written. Snapshot Summary › 1 snapshot written from 1 test suite. Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 1 written, 1 total Time: 1.429s Ran all test suites.実行すると、
tests/unit/__snapshots__
以下にsnapshotのファイルが格納される。参考
カバレッジを表示させたい場合
jest.config.js
に以下を追加する。module.exports = { ... "collectCoverage": true, "collectCoverageFrom": ["src/js/**/*.{js,vue}"] ... };追加後の
jest.config.js
module.exports = { moduleFileExtensions: ["js", "jsx", "json", "vue"], transform: { "^.+\\.vue$": "vue-jest", "^.+\\.jsx?$": "babel-jest" }, transformIgnorePatterns: ["node_modules/"], moduleNameMapper: { "^@/(.*)$": "<rootDir>/src/js/$1" }, snapshotSerializers: ["jest-serializer-vue"], testMatch: [ "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" ], testURL: "http://localhost/", "collectCoverage": true, "collectCoverageFrom": ["src/js/**/*.{js,vue}"] };エラーが出る場合
Cannot read property 'bindings' of null
とのエラーが出る場合、$ npm i -D @babel/preset-env
.babelrc
を下記の通り修正する。
{ "presets": ["env"] }
↓
{ "presets": ["@babel/preset-env"] }
Upgrade to Babel 7: Cannot read property 'bindings' of null
参考
- 投稿日:2019-06-27T13:40:43+09:00
[初心者向け]VuexのStoreを細かくモジュール分けしよう
はじめまして、PMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います。はじめに
Vuexを始めたばかりの自分はVuexに関連する処理をすべて
store.js
などの一つのファイルに記述してしまっており、ファイルが非常にファットになってしまっおり、可読性も非常に下がってしまっていました。
そこでVuexファイルを細かくモジュール分していきたいとおもいます。必要な環境
- Vue
- Vuex
実装
まず
store/
以下に任意のjsファイルを作成してください。今回はuser.js
というファイルを作成しています。user.jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const state = { //stateデータ user: null } const mutations = { //同じ処理を行うならnewMsgはobjectに setUserInfo(state, userInfo) { //任意の同期処理 state.user = userInfo } } const actions = { getUser() { //任意の非同期処理 } } export default { namespaced: true, state, actions, mutations, }このように
state
,mutations
,actions
それぞれを定数に入れてそれをexport
しています。それぞれのモジュールを集約しよう
store
ディレクトリと同じ階層に任意のjsファイルを作成してください。今回はstore/index.js
を作成します。index.jsimport Vue from 'vue' import Vuex from 'vuex' import user from './user' Vue.use(Vuex) export default new Vuex.Store({ modules: { user: user }, })ここではまず
store/
以下の作成したVuexファイルをimport user from './store/user'
でインポートします。
そしてモジュールを集約してexport
しています。main.jsで呼び出し
main.jsimport Vue from 'vue' import store from './store/index' new Vue({ render: h => h(App), store }).$mount('#app')
mian.js
内でstore/index
をインポートし、Vueインスタンス
内にインポートしたものを読み込んでください。Vuexを呼び出す際の注意点
sample.vue<script> export default { created() { this.$store.dispatch("user/getUser"); this.$store.commit("user/setUserInfo", "hoge"); this.$store.state.user.user; } }; </script>このように呼び出し先の指定が
index.js
での定義から、"user/getUser"
、"user/setUserInfo"
、state.user.user
のようになっていることに注意です。最後に
このようにVuexのファイルを細かくモジュール分けすることができました。
ファイルを細かくモジュール分けすると可読性が上がり、開発効率が上がると思います。
機会があればぜひ試してみてください。
なにか間違いなどあれば指摘していただけると幸いです。
- 投稿日:2019-06-27T12:48:37+09:00
[vue] スマホ重視な数字を選択できるコンポーネント
junban.appで作成したコンポーネントを公開していこうと思います。
ちゃんとしたコンポーネント配信が目的ではなく、プロジェクト毎にコピペして、文字とかアイコンとかはその都度カスタマイズを目的としてますので、ここslotにした方が柔軟性が~とかは考えないです。
機能概要
数字を選択できるコンポーネントです。
スマホで数字入力(input[type=number]とか)はスマホのキーボードが表示されて嫌なので作りました。
テスト環境
- "nuxt": "^2.7.1"
- lodash使います。
コンポーネント
<template lang="pug"> .select_numbers .select_number( v-for="num in selects" :class="{active: num == myValue, error: isCheckValidate && !validete}" @click="select(num)" ) .number {{num}} </template> <script> import { range } from 'lodash'; export default { props: { required: { type: Boolean, default: false, }, value: { type: Number, require: false, default: null, }, min: { type: Number, default: 0, }, max: { type: Number, default: 10, }, step: { type: Number, default: 1, }, }, data() { return { myValue: null, isCheckValidate: false, }; }, computed: { selects() { return range(this.min, this.max + 1, this.step); }, validete() { if (this.required) { if (this.myValue || this.myValue === 0) { return true; } return false; } return true; }, }, mounted() { if (this.value) this.myValue = this.value; }, methods: { select(num) { this.myValue = num; this.$emit('value', this.myValue); }, checkValidate() { this.isCheckValidate = true; return this.validete; }, }, }; </script> <style lang="sass"> // 色々カスタマイズしてたので、内容は適当で、重要そうなとこだけピックアップ .select_numbers position: relative display: flex flex-direction: row justify-content: flex-start flex-wrap: nowrap overflow-x: scroll .select_number cursor: pointer flex: 0 0 auto width: 44px </style>使い方
<template lang="pug"> ... selectNumber( :value="value" @value="answer = $event" :min="min" :max="max" :required="required" ) ... </template>バリデーション
refをつけてあげて、
this.refs.hoge.checkValidate()
とかでバリデーションできます。
- 投稿日:2019-06-27T11:42:35+09:00
JavaScriptとElmを比べてみた〜前編〜
Elmとは、JavaScriptにコンパイルできる言語、いわゆるaltJSです。
変数宣言(のようなもの)
JavaScriptの場合
const a = 1; let b = 1; var c = 1;ワイ「
const
は定数いうて再代入できひんやつやな」Elmの場合
a = 1ワイ「
const
もlet
もvar
も無いねんな」ハスケル子「はい」
ハスケル子「デフォルトで再代入不可です」
ハスケル子「不変なので、変数ですらなくて」
ハスケル子「ただ値に命名している、値を定義しているって感じですね」
ハスケル子「あとセミコロンも要りません」ワイ「再代入はできなくても、JSのconstみたいに」
ワイ「オブジェクトのプロパティを一部変更することはできんねやろ?」ハスケル子「いえ、オブジェクトのプロパティ・・・」
ハスケル子「というかElmではレコードのフィールドですね」
ハスケル子「フィールドも上書きできません1」
ハスケル子「全ての値が不変です」ワイ「へぇぇ・・・」
関数の定義
JavaScriptの場合
function add (a, b) { return a + b; }または
const add = function (a, b) { return a + b; }アロー関数式で書くと↓
const add = (a, b) => a + b;Elmの場合
add a b = a + bワイ「Elmではカッコもカンマも無いんやな」
ワイ「さらにreturnも書かへんねやな」ハスケル子「はい」
ハスケル子「関数の最後に評価された値が自動的に戻り値になります」ワイ「そもそも
function
とかいうのも無いんやな」
ワイ「変数とほぼおんなじやん」ハスケル子「そうですね」
ハスケル子「引数があれば関数って感じです」関数の実行(適用)
JavaScriptの場合
const result = add(3, 5);Elmの場合
result = add 3 5ワイ「関数を適用するにもカッコもカンマも無しなんや」
ハスケル子「はい」
ハスケル子「ただ、add
関数の結果を更に別の関数に渡したいときなんかは───」result = anotherFunc add 3 5ハスケル子「───と書くと」
ハスケル子「anotherFunc
関数に対して、add
と3
と5
という」
ハスケル子「3つの引数を渡してる感じになっちゃうので」result = anotherFunc (add 3 5)ハスケル子「↑こうすると、カッコで囲まれた部分が先に実行されます」
ワイ「なるほどな」
ワイ「add 3 5
の計算結果、つまり8
が」
ワイ「anotherFunc
関数の引数として渡される感じか」ハスケル子「はい」
ハスケル子「または───」result = anotherFunc <| add 3 5ハスケル子「───こう書いても同じです」
ワイ「あー、パイプラインいうやつやね」
ワイ「これ読みやすくて好きやわ」書く順序による影響
JavaScriptの場合
const a = 3; const b = 5; const c = a + b;ワイ「基本、上から順に実行って感じよな」
ハスケル子「はい」
ハスケル子「なので例えば───」const c = a + b; const a = 3; const b = 5;ハスケル子「───こんな感じで」
ハスケル子「a
やb
に値を代入するより上の行で」
ハスケル子「a
やb
を使った計算などをしようとすると」
ハスケル子「エラーになっちゃいますよね」ワイ「なるほどな」
ワイ「でもまあ、それは普通そうやろ」ハスケル子「それがElmの場合は違うんですよ」
Elmの場合
c = a + b a = 3 b = 5ハスケル子「↑これも普通にOKです」
ワイ「ええ・・・」
ワイ「a
やb
を定義するより上の行で」
ワイ「a
やb
を計算に使ってるやん・・・」ハスケル子「はい」
ハスケル子「関数と同じ感じなんですよ」ワイ「ああ・・・」
ワイ「JSでも関数はそうやもんな」
ワイ「下の方で宣言した関数、上の方で使えるもんな」ハスケル子「はい」
ハスケル子「Elmでは全ての値が不変なので、それが可能なんです」ワイ「全ての値が不変・・・つまり再代入という概念が存在しないということやろ?」
ワイ「それやと順番が関係なくなるの?」
ワイ「なんで・・・?」ハスケル子「JSの場合は───」
let a = 3; console.log(a); // 3 と表示。 a = 5; console.log(a); // 5 と表示。ハスケル子「
let a = 3;
って書いたすぐ下の行でa
を呼び出したら、a
の値は3
だけど」
ハスケル子「その後、a = 5;
って再代入して、その下の行でa
を呼び出したらa
の値は5
」ワイ「それは分かるわ」
ワイ「当然の時間の流れや」ハスケル子「でも再代入という概念がないとしたらどうですか?」
ワイ「ああ・・・」
ワイ「再代入できひんなら、a
はいつでも3
や」
ワイ「a
の状態が変わることが無いから」
ワイ「a
を5
に変えた後に呼び出したらどうこう・・・」
ワイ「みたいな話がそもそもあり得へんわけやな」ハスケル子「そうです」
ハスケル子「時間が止まってるようなものなので」
ハスケル子「コードの中に前とか後とか無いイメージです」ワイ「なるほどなー」
ワイ「時が止まってるような」
ワイ「1フレームの中で全てが実行されているような」
ワイ「不思議な感じやな」
ワイ「スタープラチナ・ザ・ワールドみたいやな」ハスケル子「オラオラオラオラ!って感じです」
ワイ「痛い!痛い!」
ワイ「やめてぇや」ワイ「ええと、つまり」
ワイ「時間が止まっとるようなもんやから」
ワイ「値の定義より上の行で、その値を使っても」
ワイ「問題ないんやな」ハスケル子「そんな感じです」
ワイ「でも、なんの値も変えられなくて」
ワイ「この言語、何ができるの・・・?」ハスケル子「普通にシングルページアプリケーションとか」
ハスケル子「動きのあるゲームだって作れますよ」ワイ「えぇ・・・!?」
ワイ「全て不変のはずやのにボール動いてますやん・・・!」
ワイ「言うてることちゃいますやん・・・!」ハスケル子「気になるならElm Guide読んでみてください」
ハスケル子「じゃあ」
ハスケル子「次はオブジェクトとかイベントハンドラについて比べてみましょう」〜後編に続く〜
詳しくは後編にて。 ↩
- 投稿日:2019-06-27T11:42:22+09:00
[vue] 配列の中に文字列を入れたり消したりするコンポーネント
junban.appで作成したコンポーネントを公開していこうと思います。
ちゃんとしたコンポーネント配信が目的ではなく、プロジェクト毎にコピペして、文字とかアイコンとかはその都度カスタマイズを目的としてますので、ここslotにした方が柔軟性が~とかは考えないです。
機能概要
フォーム入力バインディングした配列の中に文字列を入れたり消したりするコンポーネントです。
テスト環境
- "nuxt": "^2.7.1"
コンポーネント
<template lang="pug"> div .input( v-if="value" v-for="(inputValue, index) in myValue" :key="'input'+index" ) input.form-control( type="text" :value="inputValue" @input="v => update(index, v)" ) .remove(@click="remove(index)") span.icon i.fas.fa-minus-circle .btns button.btn.btn-primary.btn-icon-split(@click.prevent="add") span.icon.text-white i.fas.fa-plus-circle span.text テキストを追加する </template> <script> export default { props: { value: { type: Array, require: true, default: null, }, }, data() { return { // なぜかmyValueを参照すると新しく追加した時再描画してくれる? // Vueの?として新規で作るとvalueのupdateイベントに再表示が入ってない? myValue: null, }; }, mounted() { this.myValue = this.value; }, methods: { add() { this.value.push(''); }, remove(index) { this.value.splice(index, 1); }, update(index, event) { this.value[index] = event.target.value; }, }, }; </script>使い方
<template lang="pug"> ... InputTexts(v-model="textArr") ... </template>今後の課題
? 追加されないバグ
何が原因でmyValueを用意しないと追加した時に表示されないのかそこまで突き止めてない。
元のvalueの値が['hoge']
とかなら表示されるけど[]
の場合表示されないみたいな感じだったような?
時間があれば調査します?MEMO
こういうのをGistやQiitaとかで管理したいけど
- Gist: 動画、説明載せれない
- Qiita: コード部分のバージョン管理ができない
- Github: 面倒
QiitaにGistが貼れたら解決しそう
- 投稿日:2019-06-27T11:42:22+09:00
[vue] フォーム入力バインディングした配列の中に文字列を入れたり消したりするコンポーネント
junban.appで作成したコンポーネントを公開していこうと思います。
ちゃんとしたコンポーネント配信が目的ではなく、プロジェクト毎にコピペして、文字とかアイコンとかはその都度カスタマイズを目的としてますので、ここslotにした方が柔軟性が~とかは考えないです。
機能概要
フォーム入力バインディングした配列の中に文字列を入れたり消したりするコンポーネントです。
テスト環境
- "nuxt": "^2.7.1"
コンポーネント
<template lang="pug"> div .input( v-if="value" v-for="(inputValue, index) in myValue" :key="'input'+index" ) input.form-control( type="text" :value="inputValue" @input="v => update(index, v)" ) .remove(@click="remove(index)") span.icon i.fas.fa-minus-circle .btns button.btn.btn-primary.btn-icon-split(@click.prevent="add") span.icon.text-white i.fas.fa-plus-circle span.text テキストを追加する </template> <script> export default { props: { value: { type: Array, require: true, default: null, }, }, data() { return { // なぜかmyValueを参照すると新しく追加した時再描画してくれる? // Vueの?として新規で作るとvalueのupdateイベントに再表示が入ってない? myValue: null, }; }, mounted() { this.myValue = this.value; }, methods: { add() { this.value.push(''); }, remove(index) { this.value.splice(index, 1); }, update(index, event) { this.value[index] = event.target.value; }, }, }; </script>使い方
<template lang="pug"> ... InputTexts(v-model="textArr") ... </template>今後の課題
? 追加されないバグ
何が原因でmyValueを用意しないと追加した時に表示されないのかそこまで突き止めてない。
元のvalueの値が['hoge']
とかなら表示されるけど[]
の場合表示されないみたいな感じだったような?
時間があれば調査します?MEMO
こういうのをGistやQiitaとかで管理したいけど
- Gist: 動画、説明載せれない
- Qiita: コード部分のバージョン管理ができない
- Github: 面倒
QiitaにGistが貼れたら解決しそう
- 投稿日:2019-06-27T01:53:00+09:00
Vue.jsとonsenuiを使ってスライダーメニューを作って見よう
はじめまして、PMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います。はじめに
今回はonsenuiというネイティブライクなUIを作る事のできるCSSフレームワークを使ってスライダーメニューを作って行きたいと思います。
onsenuiはVue.jsにも対応しています。Vue.jsの他にはjQuery、AngularJS 1.x、AngularJS 2+、Reactなどにも対応しています。出来上がりはこんな感じ
https://www.youtube.com/embed/RKuCXmh_3q8
必要な環境
- Vue.js
- Vuex
実装
インストール
- onsenuiのインストール(今回はVue.js対応のmoduleをインストールします)
npm install vue-onsenui -s
npm install onsenui -s
- Vuexのインストール
npm install vuex -s
画面の作成
App.vueに以下を追加してください
App.vue<template> <v-ons-page id="app"> <v-ons-splitter> <v-ons-splitter-side swipeable collapse width="250px" :animation="$ons.platform.isAndroid() ? 'overlay' : 'reveal'" :open.sync="menuIsOpen" > <menu-page></menu-page> </v-ons-splitter-side> <v-ons-splitter-content> <router-view></router-view> </v-ons-splitter-content> </v-ons-splitter> </v-ons-page> </template>ここでスライドメニューの制御を行って行きます。スライドメニューのおおもとになる部分だと思ってください。
次にサイドメニューのcomponentを作っていきます。components/
以下に任意のコンポーネントを作ってください。今回はcomponents/Sidemenu.vue
という感じで作成しました。Sidemenu.vue<template> <v-ons-page> <v-ons-toolbar modifier="transparent"></v-ons-toolbar> <div class="header"></div> <v-ons-list-title>Sample</v-ons-list-title> <v-ons-list> <v-ons-list> <v-ons-list-item modifier="chevron"> <div class="left"> <v-ons-icon icon="md-home"></v-ons-icon> </div> <div class="center">hoge1</div> </v-ons-list-item> <v-ons-list-item modifier="chevron"> <div class="left"> <v-ons-icon icon="md-home"></v-ons-icon> </div> <div class="center">hoge2</div> </v-ons-list-item> <v-ons-list-item modifier="chevron"> <div class="left"> <v-ons-icon icon="md-home"></v-ons-icon> </div> <div class="center">hoge3</div> </v-ons-list-item> </v-ons-list> </v-ons-list> </v-ons-page>次は実際にメニューバーを描画するconponentを作っていきます。今回はvue-cliなどで作成される
components/HelloWorld.vue
に以下を記述していきます。HelloWorld.vue<template> <v-ons-page> <v-ons-toolbar class="home-toolbar"> <div class="left"> <v-ons-toolbar-button @click="$store.commit('splitter/toggle')"> <v-ons-icon icon="ion-navicon, material:md-menu"></v-ons-icon> </v-ons-toolbar-button> </div> <div class="center">{{ msg }}</div> </v-ons-toolbar> </v-ons-page> </template>ここで
<v-ons-icon icon="ion-navicon, material:md-menu"></v-ons-icon>
でメニューバーを描画しています。
main.js
に以下を追記してくださいmain.jsimport Vue from 'vue' import App from './App' import VueOnsen from 'vue-onsenui'; import 'onsenui/css/onsenui.css' import 'onsenui/css/onsen-css-components.css' import store from './store' Vue.use(VueOnsen); Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, store, components: { App, }, template: '<App/>' })そして
src/
にstore.js
というファイルを作成してください。基本的にどこに作成しても構いませんがmain.jsでstore.jsを読み込むディレクトリが変わってきますのでそこだけ注意してください。
store.js
に以下を追記してください。store.jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ modules: { splitter: { namespaced: true, state: { open: false }, mutations: { toggle(state, shouldOpen) { if (typeof shouldOpen === 'boolean') { state.open = shouldOpen } else { state.open = !state.open } } } } } })確認してみる
動きを付けていく
この状態ではハンバーガーメニューを押しても何も反応しません。
ここからは動きを付けていきたいと思います。App.vueに以下を追記してください。App.vue<script> import MenuPage from "./components/Sidemenu"; export default { name: "App", components: { MenuPage }, computed: { menuIsOpen: { get() { return this.$store.state.splitter.open; }, set(newValue) { this.$store.commit("splitter/toggle", newValue); } } } }; </script>最後に
これで無事サンプルのような動きが実現できたと思います。
今回はvuexを使っているので少し難しく見えるかもしれませんがぜひチャレンジしてみてください。
間違いなどあれば指摘していただけると幸いです。
- 投稿日:2019-06-27T01:48:24+09:00
【備忘録】Vue.jsをNetlifyにホスティングするときのリダイレクト設定
※自分用。SPA勉強中の初心者。
経緯
vue.jsでのフロント側でのルーティング設定で、vue-routerを用いていたが、Netlifyにデプロイして、トップ以外のページのURLを叩いた時に404になってしまい、困ったので調べた。
調べた
公式に書いてあった。
サーバーの設定例
にあるように、設定ファイル等ケースにより対応が必要らしい。Redirects | Netlifyをみると、
_redirects
というファイルをindex.html
と同階層に設置すればいいらしい。_redirects/* /index.html 200上記で問題なく、想定通りのルーティングで動作を確認した。
また、静的ファイル置き場の
public
ディレクトリの中に_redirects
を入れておくことで、デプロイ時の自動ビルドでも問題なくそのまま吐き出されていた。参考
- Netlify redirectsのtestページ:https://play.netlify.com/redirects
- https://becolomochi.hatenablog.com/entry/2019/01/08/233027
- 投稿日:2019-06-27T00:35:06+09:00
Vue + Vue Router + Vuex + Laravel 写真共有アプリチュートリアルの環境をDockerで構築する
はじめに
今年1月にQiitaに投稿されて以来、1,500以上のいいねを獲得している下記の人気チュートリアル。
Laravel + Vue.jsでのSPA開発のほか、Laravelでのテストコードの書き方など幅広い知識が同時に学べ、非常にオススメです。
このチュートリアルでは、開発環境構築にLaravel Valetを使っていますが、私の場合はDocker(Laradock)で環境を構築しました。
Dockerで環境を構築したい方は、参考にしてみてください。
環境
- macOS High Sierra 10.13.6
- Docker 18.09.2
ディレクトリ構造
本記事での環境構築を終えると、下記のディレクトリ構造が出来上がります。
├── data // データベースのディレクトリ ├── laradock // Docker関連ファイルのディレクトリ └── vuesplash // Laravelのルートディレクトリ環境構築手順
本記事での環境構築手順は、
Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (3) SPA開発環境とVue Router
の
「Laravelプロジェクトを作成する」 〜 「フロントエンドの準備」の直前
までに相当します。
チュートリアルの
までを読み進めたら、本記事での環境構築を実施し、その後は「フロントエンドの準備」から先を進めてください。
1. Laradockのコピー
プロジェクトのルートとなるディレクトリに、Laradockをコピーします。
$ git clone https://github.com/Laradock/laradock.git2. Laradockの.envファイルの編集
laradock
ディレクトリが出来上がるので、その配下のenv-example
ファイルをコピーし、.env
ファイルを作成します。$ cd laradock laradock $ cp env-example .env
laradock/.env
の以下2つの設定を編集します。laradock/.envAPP_CODE_PATH_HOST=../vuesplash DATA_PATH_HOST=../data3. PostgreSQLのバージョンの指定
チュートリアルではPostgreSQLのバージョン指定はありませんが、9.6.12を使用することにします。
PostgreSQLのDockerファイルを以下の通り編集します。laradock/postgres/DockerfileFROM postgres:9.6.12 LABEL maintainer="Ben M <git@bmagg.com>" CMD ["postgres"] EXPOSE 54324. コンテナのビルド
コンテナをビルドします。
laradock $ docker-compose up -d --build workspace postgres php-fpm nginx5. Laravelのインストール
Laravelをインストールします。
バージョンはチュートリアルで使用されている5.7.19
にします。下記コマンドを実行してください。
laravel/laravel . "5.7.19"
の部分にある.
を忘れずに入力してください。laradock $ docker-compose exec workspace composer create-project --prefer-dist laravel/laravel . "5.7.19"6. Laravelの.envファイルの編集
Laravelの.envファイルを編集し、LaravelからPostgreSQLに接続できるようにします。
vuesplash/.envDB_CONNECTION=pgsql DB_HOST=postgres DB_PORT=5432 DB_DATABASE=sample DB_USERNAME=default DB_PASSWORD=secret7. データベースを作成する
まず、PostgreSQLに接続します。
laradock $ docker-compose exec workspace psql -U default -h postgresパスワードを入力します。
Password for user default:続いて、データベースを作成します。
データベース名は、チュートリアルで指定されているvuesplash
にします。default=# create database vuesplash;PostgreSQLとの接続を終了します。
default=# \q8. Laravelのlocale設定を日本にする
vuesplash/config/app.php<?php return [ // 略 'locale' => 'ja', // 略 ];9. Laravelの動作確認
http://localhost/
にブラウザでアクセスし、LaravelのWelcome画面が表示されることを確認してください。10. 後はチュートリアルを再開してください
ここまでの作業が完了したら、チュートリアルの「フロントエンドの準備」から先を進めてください。
なお、チュートリアル記事中でコマンドを実行するよう記載があった場合は、例えばそれが
npm install
であればlaradock $ docker-compose exec workspace npm installといった具合に実行すればOKです。
最後に
以上です。
本記事が、この素晴らしいチュートリアルに取り組む方のお役に立てば幸いです
- 投稿日:2019-06-27T00:08:54+09:00
Nuxt.jsとAWSで招待状webページを作ったまとめ
概要
2018年に結婚しました!ので!
ここはエンジニアらしくパーティ招待状Webページをつくってみることにしたのが始まりです。とりあえず無事にパーティも終わったので、開発時の記憶をさかのぼりながら残す備忘録ですが
申請から開発まで一通り殴り書くので、何かしら参考になれば幸いです。┗(^o^)┛
…ちょっと前の記憶を掘り起こしながらやるので、間違って書いてそうなところもある気がしますが温かい目で御覧ください開発環境はMacなので、Winの方は適宜読み替えていただけると幸いです
AWS上で日本語表示できている部分は、日本語の画面で説明してる…はずです成果物
webページ
ざっくり3つの画面構成です。(3つ目はフリー素材やOSSの情報なので割愛)
GoogleMapや開催日を記載したホーム画面と、実際に参加者の情報を登録してもらう登録画面の2つです。
ホーム画面 登録画面 システム構成
Route53でドメインをとったうえで、「必要なときに必要な程度稼働してくれる」実運用を考えながら、今回そこまでやる必要はないシステムを作ってます。先人たちの知恵借りまくりです。
Github
書いたコードは、以下の2つ。
システム リンク webページ https://github.com/tyabata/web-invite api(lambda) https://github.com/tyabata/lambda-api-invite 個人情報とかcommitに入れちゃったのでgitのcommitログだけ消し去ってます
採用した技術
関連キーワードの羅列。詳細は次項から説明します。
Server Side
AWS
Lambda
DynamoDB
API Gateway
CloudFront
Route53
S3
理由
ちょっと前に自作IoTでGCP使ったので今回はAWS。
業務でスマートスピーカー開発してたときに触ったけど雰囲気でやってたので
復習でもしようかなという気持ちで選択しました。Client Side
Nuxt.js
Vue.js
Vuex
Vuetify
TypeScript
axios
PWA
PostCSS
直近の業務で、React + Reduxを使っていたので今回はVue。
上記にいろいろ陳列してますがnuxt-ts
でほぼ全て用意してるので、PostCSS以外はだいたいコマンド一発。それが今年の1月末の頃…
nuxt-tsは2019/4/5ぐらいにnuxtに統合され 導入方法が変わっています
https://github.com/nuxt/nuxt.js/releases/tag/v2.6.0Nuxt v2.4.0 is finally here ?
— Nuxt.js (@nuxt_js) 2019年1月28日
Official TypeScript support, smart prefetching and many more features & bug fixes.https://t.co/Ljf29xYvXi「nuxtのconfigにtsの設定いれるのつら」
と思いながら格闘して環境構築完了した後日(2019/1末頃)、Nuxt公式が上をツイートしててnuxt-ts
を知り、結果的にはほぼ何もせず 「TypeScript」で「Vue+Vuex」を作る環境ができました。型はいいぞぉ
型により構造の把握が楽になるし、ちゃんと書けてれば静的に問題に気付けるし以下略開発手順概要
とりあえずAWSで登録をすませます
https://aws.amazon.com/jp/register-flow/複数人で開発するならIAMとかで管理アカウントと分けましょう。と言いたいとこですが
今回は一人で かつ お仕事ではないので端折ります。Server Side
まずは「参加者情報を登録するAPI」「Webサーバの代わりにs3を使う」という流れ
Route53
でドメイン取得 ->Certificate Manager
でSSL証明書取得s3
準備 (linkのみ紹介)aws-cli
の導入Lambda
準備からDynamoDB
にデータ登録までAPI Gateway
とLambda
を接続CloudFront
でS3
とAPI Gateway
のマルチオリジンにバックポストする際の設定必要なとき以外、見る必要も見られることもない招待ページなので
コンピューティング時間を減らして省エネ運用の構成をとって…いるように見せかけてやたら色々準備したのは勉強がてら実際に使うことを考えた構成を目指してみたという具合です。Client Side
Nuxt.jsを利用してページを開発 -> index.htmlを出力してS3にアップロードするまで
を以下の手順で説明していきます。
nuxt-ts
で開発できる下準備- ページを作る
- index.htmlを出力する
- S3にアップロードする
開発詳細 : Server Side
ここからが本題です
ドメイン取得と証明書作成まで
Route53でドメイン取得
まずはドメインを取得します。
新しいドメインの登録 - Amazon Route 53
- https://console.aws.amazon.com/route53/ でRoute53を開く
- 登録方法
- 初めて : [Domain Registration] の [Get Started Now]を選択
- 二回目以降 : [Registered Domains]を選択
画像は省きますが、基本は同じ。
リンク先の説明にならって作業をすれば、自分のドメインがつくれます。
トップドメインによってお値段が異なるので、今回は安価でよくみる.net
を選択
- トップドメイン以降の自分でほしい名前を入力
- チェックの結果がOKであれば、それをカートに入れる (
Add to cart
)- 連絡先などを入力して進むと登録完了
- 登録したドメインが [Domain registration in progress]の状態からしばらく待つと[All Contacts]になれば完了。SOAレコードとかも同時に作成済みの状態になります。
ドメイン作成はここまでですが、これだけは登録したタイミングで課金が発生します。
Certificate Managerで証明書作成
さすがにwebエンジニアとしてhttpのリンクで友人だけでなく、嫁の知人含めて招待ページ登録してねー。と公開するのは社会の窓全開でご挨拶してる気がするのでサボらずちゃんと作成します。
初回は
[Provision certificates(証明書のプロビジョニング)] => [Get Started(今すぐ始める)] => [Request a Certificated(署名書のリクエスト)]
と選択していくと以下のような画面になると思います。ドメイン名の入力欄に先程登録したドメインを入れましょう。
ここではワイルドカード証明書のリクエストもできるので、私は*.hoge.net
のような名前で証明書を作成しました。
この後、進めていくとドメイン所有者(つまり自分)に下記のようなメールが飛びます。
DomainやAccountIDや取得したRegionなどに問題がなければ、メールに記載されている
To approve this request, go to~
と書いてあるあたりのリンクから遷移して承認完了させます。
下記のような画面まで行けば、証明書の作成まで完了です。
実際に証明書を設定したりするのはCloudFrontあたりを扱う項へ。
S3の準備
ほか項目含めて全部書くと、やたら長いドキュメントになるので備忘録としてリンクだけ。
S3 バケットを作成する方法 - Amazon Simple Storage Serviceバケット作成後に追加で設定したものは
- バージョニングの有効化 + ライフサイクルから旧バージョンに対する削除の設定
- 本番リリース後に問題発覚して戻すことがある場合、バージョン指定で戻せる
- しかし一度動いてしまえば1日以上たっても変わらない
- cliコマンドからのデプロイするために対象アカウントのみ書き込みを有効化
- それ以外の全ユーザーに対してはReadのみ有効
ライフサイクルの設定については、作成したバケットの上部にある「管理」タブから
[ライフサイクルルールの追加]という項目からできます。
上記通り、一時的なロールバックを考慮してバージョニングの有効化をしたので
一日以上たった過去バージョンは削除する。というライフサイクルを設定しました。
(結果的に不要でした)アクセス権限については、CloudFrontからアクセスが前提なので
本当は全ユーザー有効設定ではなくCloudFrontからのアクセスに対してReadを与えるような設定が良いと思います。
…色々調べながらやってたので、全ユーザーがreadできる方が都合がよかったのです… w
(といってもさすがにURLは公開してないです)とりあえずこのタイミングでは、表示確認のための
index.html
に適当になんか書いたものをバケットのrootにおいといてください。設定についてはこちらを参考にしてみてください。
CloudFront ディストリビューションからのみ S3 バケットへのアクセスを許可するアクセス権限は適切にね!!
aws-cliの準備
Lambdaやs3にデプロイをするために、AWS用のコマンドラインツールである
aws-cli
からデプロイする準備をします。IAMでユーザーの作成
aws-cliでアクセスする際に使うユーザー設定を行います。
admin使ってもできるんですが、お勉強とお作法的に分けます。下記リンク等を参考に必要な情報を作成します。
最初の IAM 管理者のユーザーおよびグループの作成 - AWS Identity and Access ManagementIAMから、左カラムのナビゲーションメニューから
ユーザー
を選択し、上部にあるユーザーを追加
を選択します。
すると下の画像のように、ユーザーを追加するための設定画面が出てくるので、下の表のように設定していってください。
項目 設定 補足 ユーザー名 cli ※なんでもOK アクセスの種類 プログラムによるアクセス グループの作成 あとで説明 既存のポリシーを直接アタッチすることもできます タグ なし IAMの管理用です。個人開発で特にいらないので今回は省略 ここまで入力すると、確認の表示が出てくるので問題なければ次に進むと以下のようにアクセスキーとシークレットアクセスキーが取得できます。
どこかにメモっておきましょう。あとのcli設定に使いますIAMの概念(雑まとめ
グループ作成について説明をしていきます。
AWSに初めて触るとロール
ユーザー
グループ
ポリシー
といろんな言葉がでてきて
チンプンカンプンになる(な気もしてる)ので、私の雑まとめです。
- ポリシーは 「AというポリシーはDynamoへのRead権限をもつ」といったルール的なもの
- ユーザー、グループ、ロールはそれぞれポリシーをアタッチして使う
- ユーザーやグループは人の管理に使う
- ロールはシステムで使う
という雰囲気理解です。とりあえず関係性が雰囲気でもわかればOKかと。
上の画像で線を足し忘れましたが、ユーザーにポリシーをアタッチできます。
が、複数で開発する場合は「開発」というグループをつくってユーザーをそこに紐づけていくことで、わざわざ個別にポリシーをアタッチする手間を一つにまとめられる利点等があると思います。
- Lambdaにデプロイするには
AWSLambdaFullAccess
ポリシー- S3にデプロイするには
AmazonS3FullAccess
ポリシーといった具合で今回は
項目 設定値 ユーザー名 cli グループ名 deployment ポリシー(グループに対して) AWSLambdaFullAccess,AmazonS3FullAccess を設定しています。
「勉強だしとりあえず全権限ふっておけ」といった場合は
AdministratorAccess
を設定すればOKです。
仕事でやるなら用法用量はまもりまs(ryaws-cli導入
brewで入れました。brew update行ってinstallから始めていきます。
brew install awscli [~] aws --version 13:29:19 aws-cli/1.16.80 Python/3.7.1 Darwin/18.0.0 botocore/1.12.70次にアクセスキーの設定をします。
[~] aws configure 14:52:40 AWS Access Key ID [****************XXXX]: AWS Secret Access Key [****************XXXX]: Default region name [us-east-1]: Default output format [None]:
Access Key ID
やAWS Secret Access Key
は先程取得した値を設定してください。
regionはとりあえずTokyo(ap-northeast-1)とかでもいいと思います。
Outputはjson
やText
とかありますが、これはご随意に(説明略)[~] aws s3 ls 14:52:50 2018-12-24 20:36:54 hoge.xxxx.netといったように確認コマンドで結果が返ってくればOKです。
LambdaへのdeployとdynamoDBの設定
とうとうコードがでてきますが、API側はだいぶシンプルに書いたつもりなので概要だけ。
https://github.com/tyabata/lambda-api-inviteAPIのロジック
- Lambdaに対してリクエスト
/invitees/user
へのGET と/invitees
へのPUTを処理する- 実行例外は拾って、500を返すようにしている
/invitees/user
: GET
- ページを初めて開いたときにアクセスされる。サーバ側でもつパスワードでcryptoした文字列をUserIDとして返す。UUIDは
crypto-js
を利用して時間から生成してる- ページ側がlocalstorageで保存しているので、消さない限りは再リクエストは発生しない(が、消されると新しいユーザーとして再度UUIDを発行します)
- webページ用にサーバを建てないようにした結果。こうなった
/invitees
: PUT
- ページに登録した情報とUserIDをセットにして登録リクエスト
- UserIDが正しく複合できるか確認
- 問題なければ、登録情報が正しい値かチェックする
- すべてOKであればDynamoDBに登録
という流れ。
特に嫁側の方に登録時のハードルを上げないようにしたかったので、下を意識してざっくり作りました。
- ログインせずに登録できる。
- 一度画面閉じたあとに登録情報を更新できるようにidをもたせておきたい
- 私の友達が 絶対
いたずらデバッグするので簡単に登録APIを叩かれないようにしたlambdaへのデプロイ
シンプルなAPIで aws-cliもいれたので、package.jsonに以下のように記述
"zip": "zip lambda.zip -r node_modules src", "first": "aws lambda create-function --function-name <lambdaに登録するfunction名> --zip-file fileb://lambda.zip --region <登録するlambdaのregion> --handler src/index.handler --runtime nodejs8.10 --role <登録するfunctionにアタッチするrole>"これは新規登録用。nodeとかは古いので適当に置き換えてください
アタッチしているroleについては後で説明します。コードができたら
zip化
->first
でアップロード&function作成
をしています。"deploy": "npm run zip && npm run upload", "upload": "aws lambda update-function-code --function-name <lambdaに登録したfunction名> --zip-file fileb://lambda.zip --profile cli --publish",こちらは更新用
一つ上のzip化するコマンドにあわせて、awsにアップロードしてfunctionを更新するupload
コマンド。
そして、それらを一発で行うためのdeploy
コマンドです。ここは特別に複雑なことはしてないです。
前項のaws-cli導入
でAWSLambdaFullAccess
がポリシーとしてアタッチされている状態であれば、これで新規登録や更新が完了します。
Lambdaで登録したregionで開くと、上の通りに登録されていることが確認できると思います。そしてLambdaにアタッチしているroleですが、
CloudWatchにログを流す
DynamoにアクセスしてR/Wする
といったポリシーをふったroleをアタッチしています。詳細説明は省きます
ただ、Lambdaのログを流すために設定しておくことで、問題があったときにも気づけるので登録しておきましょう。
Amazon CloudWatch Logs とは - Amazon CloudWatch LogsDynamoDBへ登録
DynamoDBにデータを登録していく準備をします。
上記のリンクからテーブルの作成
を選び、テーブル名やプライマリキーを設定して作成を押します。
今回は、前項あたりで説明した UserIDをプライマリキーに設定するため、uid:文字列
として設定しました。
…うろ覚えですが、コレ以上はDynamoDBでの設定はなかったはずです。コード上ではここらへん
https://github.com/tyabata/lambda-api-invite/blob/master/src/dynamo.js#L30普通のDBみたいにテーブル定義がどうとか行わなくても、データをputすればそのobjectの要素通りにカラムを自動で作ります。そこらへんはお手軽。
ただ、PUTするときに空文字とかを入れようとするとエラーになるので、チェックして弾いてあげるか
設定されていないときのデフォルト値が必要であれば、登録前に足してあげましょう。const AWS = require('aws-sdk'); // 登録先のregionを設定する AWS.config.update({ region: 'us-east-1' }); const docClient = new AWS.DynamoDB.DocumentClient(); docClient.put( { // テーブル名 TableName: "table名をここに", Item: item }, (error, data) => { // 割愛コード概要はこんな感じです。登録などに必要な権限的なものは、実行しているLambdaにアタッチされているロールのポリシーに依存しています。
前項でIAMでユーザーを作ったときと同様に
- IAMの左カラムの
ロール
からロールを作成
- ロールを使用するサービスとして Lambdaを選択
- ロールにアタッチするポリシーとして
AmazonDynamoDBFullAccess
をセット- タグは任意(管理用です)
- 最後にロール名とロールの説明を書いて完了。
ちなみにFullAccessはいらないぜ。といった場合は、他にもロールがあるのですが適切なものがない場合は独自でポリシーを作成することもできます。が、これもここでは省略します。
動作確認
とりあえずここまでちゃんと設定できているか確認のためにテストしてみましょう。
直接リクエスト送るには、この先のAPI Gatewayへの登録などが必要になります。
しかし、Lambdaのテストを使って、試しに処理を実行させることは可能です。
Lambdaの関数の画面から、右上のテストボタンの左にあるプルダウンを選択しテストイベントの設定
を押します。
そこで、画像のようにhandlerが受け付けたときに処理するbodyを用意してあげることで 疎通確認とDynamoへの登録の確認ができると思います。API GatewayとLambdaを接続
API Gatewayを使い、そのバックポスト先として先程作成したLambdaのfunctionのARN(Amazon Resource Name)を指定することで、外からhttpリクエストでLambdaを実行できるようになります。
API Gatewayを選択すると、下のように新規作成時の画面が表示されます。
ここではREST APIと 新しいAPIを選択し、API名を入力して作成を開始します。
APIのパスとメソッドの設定
API作成後に上記のような画面になるのでアクション
からリソースの作成
を選択します。
- リソース名 : 適切な名前
- リソースパス : 今回は
/invitees
をすることで、リソース一覧にパスが追加されます。
/invitees
以下にパスを設定する場合は選択した状態で、上のようにアクション
から作成を同じ手順で行います。リクエストメソッドを足す場合も
アクション
からメソッドの作成
を選ぶと、一覧に新しいプルダウンが表示されるのでメソッドを選ぶと、下記のような画面が表示されるので
今回は 総合タイプにLambda関数
。Lambda関数に 先程作成したLambda functionのARNを設定してください。ARNは、Lambdaのfunctionの画面右上にある文字列で
arn:aws:lambda:us-east-1:111111111111:function:invitees_prod
みたいな形式のものです。
その他、regionなどはLambdaにあわせて設定して保存を押すと作成完了です。リクエスト/レスポンスの詳細設定
ここから、API Gatewayの詳細を設定していきます。
今回は作ったもののうち、PUT : /invitees
について説明していきます
項目 概要 メソッドリクエスト 作成したタイミングの値が入ってる。今回はこのまま 総合リクエスト リクエスト情報の制御的なところ。マッピングテンプレートをいじります(後ほど) Lambda 設定されているLambda functionの名前が表示されていればOK 総合レスポンス 返すレスポンスを制御するところ。ここに400や500のパターンを追加する(後ほど) メソッドレスポンス こちらも200, 400, 500の3つを追加しておく リクエストのマッピングについて
コレはパッと身よくわからなくてつらかったので、ここでは概要だけ。
詳細はリファレンスや先人の知恵をおかりするのをオススメします。
API Gateway マッピングテンプレートとアクセスのログ記録の変数リファレンス - Amazon API Gateway上記で設定した項目のうち 総合リクエストのを選択し、下部にあるマッピングテンプレートの項目を選びます。
「テンプレートが定義されていない場合」を選択し、
Content-Type
にapplication/json
を選びます。追加すると、さらに下にテンプレートを入力する欄が表示されるので、今回は下のように入力しました。
{ "method": "$context.httpMethod", "body" : $input.json('$'), "path" : "$context.resourcePath", "headers": { #foreach($param in $input.params().header.keySet()) "$param": "$util.escapeJavaScript($input.params().header.get($param))" #if($foreach.hasNext),#end #end } }とりあえずわからなくてもいいです。私もドキュメントにならった説明しかできなないです。
拡張したくなったらリファレンスを読みましょう
雑にいうとAPI Gatewayで受けたリクエストを展開してLambdaのコードで受けやすい形に変換してるといったところになると思います。上のようにテンプレートにはめ込んだものを handlerの第一引数で受け取って処理を実行しています。
https://github.com/tyabata/lambda-api-invite/blob/master/src/index.js#L22レスポンスのマッピングについて
Lambdaがいくらエラーメッセージを返しても、API Gatewayは200を返してしまいます。
そこで、特定の文字列が含まれるときは400
や500
で返せるように設定します。ココらへんはシンプルに
.*"status" *: *400.*
という文字列がレスポンスに含まれていれば 400を返す
といった設定です。500も同様。次に一つページをもどって、全体の画面からメソッドレスポンスを選択し、HTTPのステータスという項目に400や500などを追加します。
これで、リクエストに対して適切なステータスコードを返せるようになります。APIのデプロイ
忘れがちですが、これをやらないと反映されないので、メソッド作成等と同様にアクションから APIのデプロイを押しましょう。
デプロイ対象のステージが表示されるので、選択してデプロイ
を押すことで初めて反映されます。…ステージの説明を書き忘れていましたが、コンソールの左にAPIごとにステージという項目があり
ソレを選択して、beta
とかprod
とかステージを分けておくと、開発環境と本番環境を分けておくことができます。例えば
beta
ステージの場合は、下のようなパスになります。
https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/beta
動作確認
最後に動作確認として、メソッドの実行画面の左側にある 雷マークがついた
テスト
を選択してテストを行います。
メソッドテストと表示されている画面に遷移したら、一番したのリクエスト本文
の項目に
リクエストするbodyの情報をセットします。右側に実行時のログが表示されます。ただしく設定ができていれば、成功時のレスポンス内容がログに表示されると思います。
CloudFrontでS3とAPI Gatewayにバックポストする
やっとServer Sideの説明の最後に来ました…! 総集編?です。
取得したドメインを設定したCloud Frontでリクエストを受け取って、s3からhtmlを返すか、APIにリクエストするかをパスによってバックポストする設定を追加していきます。
2つのオリジンの追加
- Cloud Frontを開いて、
Create Distribution
から作成Web
のGet Started
を選択Origin Domain Name
にAPI Gatewayの設定を追加していきます。https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/beta
というURLなので下の通りに設定する
- Origin Domain Name
xxxxxxxx.execute-api.us-east-1.amazonaws.com
- Origin Path
/beta
- Origin IDは勝手に入力されるやつのまま
- Origin Protocol Policyは
HTTPS Only
- Behavior Settingsは以下の通り
- Viewer Protocol Policy
Redirect HTTP to HTTPS
- Allowed HTTP Methods GETとPUTが選択できる一番下の項目
- Distribution Settings
- Default Root Object :
http:web.xxx.net/
にアクセスしたときに/index.html
にアクセスするように設定する何か漏らしてる気がするけど、これで一旦作成を完了させる。
これでhomeに新しく作成されたDistributionが表示される。
作成完了には少し時間がかかりますが、作成が完了したら次にCreate Origin
でs3の設定を足していきます
Create Origin
を選択- Origin Domain Name でs3のオリジンを選択する(サジェストで表示されます)
- Origin Access Identityで
Create a New Identiy
を選択する。(理想 : こうすることで、s3へのアクセスをcloud frontのみにできる。が、今回はやってないです)これでs3のオリジンも追加しました。
アクセスしたパスでバックポストするオリジンを分ける
次に
Origins and Origin Groups
の隣のBehaviors
タブを選択して
Create behavior
を追加します。今回は、jsやcssなどをs3に置くので API以外のパスはすべてs3に流れるように設定します
で設定したのが以下の2つのパス
Default(*)
: S3にバックポストする
- Viewer Protocol Policy は
Redirect HTTP to HTTPS
- Allowed HTTP Methodsは
GET HEAD
- Cache Based on Selected Request Headersは whitelist
- Originとリクエスト情報だけforwardする(しなくてもいいけど)
invitees/*
: API Gateway にバックポストする
- GETとPUTは受け付けられるように
- Cache Based on Selected Request Headersは whitelist
- Originと一応Authorizationだけforwardしてる
- キャッシュがじゃまになるAPIしかないので、cacheの設定は問題ないように修正。
CNAMEや証明書の設定
最初に作った証明書をやっと使うときが来ました。
DistributionのGeneral
タブから、Editボタンを押し設定を編集していきます。
- CNAME(Alternate Domain Names) :
web.xxxx.net
といった証明書にあわせて自分がつけたホスト名を設定- SSL Certificate :
Custom SSL Certificate
と書いてるほうを選択して先程作成した証明書を選択する。ソレ以外はだいたいデフォの設定のままでOK
おまけ Error Pageを設定
最後におまけで、Error Pageの設定をします。
ほぼ全部のパスがs3に流れるが、適当なパスを入れるとs3のエラー画面が表示されてしまいs3使ってる感がモロバレなので、設定をします。Create Custom Error Responseを選択して
- HTTP Error Code :
403
- Error Caching Minimum TTL : よしなに
- Customize Error Response :
YES
- Response Page Path :
/error.html
- HTTP Response Code :
404
s3がなにもないパスにアクセスしたときに403を返すので、これで代わりにerror用のhtmlを表示させることができます。
動作確認
おつかれさまでした!備忘録的に書いてるので何か漏らしてる気がしますが、
これで問題がなければ 設定したドメインでアクセスしたら s3作成時においたindex.html
が表示でき、APIのパスにアクセスすればAPIの結果が返ってくるようになるはずです。…もし動かなかったらごめんなさい。
ググれば多分解決できる程度にn番煎じネタではあると思います…たぶん開発詳細 : Client Side
とりあえず一息ついたら次に表示系の開発をしていきましょう。
Serverと比べるとだいぶ楽かもです。一応補足
https://github.com/nuxt/nuxt.js/releases/tag/v2.6.0
この記事は、2019/04/05にv2.6がでるより前に作ったものの備忘録です。一応 nuxt-tsを削除してnuxtに置き換えてみたので、気になったら下の変更箇所を見てみてください
https://github.com/tyabata/web-invite/commit/6d99fbbdde76b84b6c8baa4d1c7b73f3a5683afaNuxtとTypeScriptの開発環境を構築する
ここは、僕が2019/2頃にやった
nuxt-ts
もすでに古いので、公式や他の皆々様が書いている記事をご覧になる方が良いと思います
https://ja.nuxtjs.org/guide/typescript/
create-nuxt-app
で作るところは一緒で下の設定になります。
(対話式で導入するモジュールを選んでいきます)> Generating Nuxt.js project in /Users/xxxxx/Documents/workspace/vscode/sample ? Project name sample ? Project description My sensational Nuxt.js project ? Use a custom server framework none ? Choose features to install Progressive Web App (PWA) Support, Linter / Formatter, Prettier, Axios ? Use a custom UI framework vuetify ? Use a custom test framework jest ? Choose rendering mode Single Page App ? Author name xxxxx ? Choose a package manager yarnTypeScript対応はここから下を入れてjsをtsに入れ替えていく作業になると思います。
yarn add ts-node @nuxt/typescript簡単なページはこれで完結できるのですが
webpackがラップされてる感じなのでアレコレ拡張するのが煩わしく感じることもあると思うので、プロダクトにあわせて使う使わないは判断すれば良いかなと思います。ページを作る
といっても、基本はVueとVuexです。
- Nuxtにのっとって書く
- axiosで用意したAPIと通信をする
- Vuetifyで基本的なデザイン作成
- ロジックの分離を意識する
といったぐらいのことしか…やってないかも
とりあえず書きます!create-nuxt-appから少しだけ設定を変える
create-nuxt-app
をつくると、ルートにstore
やpage
といったフォルダができます。
ちょっぱやで作る分にはいいのですが、気になる人(自分含む)は
例えばsrc
というフォルダ以下に移したい場合はsrcDir
というフィールドに設定します。nuxt.config.tsconst config: NuxtConfiguration = { srcDir: 'src/', mode: 'spa', ...また、cssを書く用にPostCSSを入れたかったので
ここに、ページで使うcssを設定
https://github.com/tyabata/web-invite/blob/master/nuxt.config.ts#L47そして
nuxt.config.js
のbuild以下に下のように設定を入れました。nuxt.config.tsbuild: { extend(config: any, context: any) { }, cssSourceMap: true, postcss: { plugins: { 'postcss-import': {}, 'postcss-mixins': {}, 'postcss-preset-env': {}, 'postcss-nested': {}, // css minify csswring: {} } } }これで設定は完了です。このアプリでは
src/assets/postcsss
以下にPostCSSを使ったcss郡がおいてあります。
…post cssをassetは違う気がするなちなみに テンプレのvueファイルでPostCSSをつかった記述をする場合は下の通りにstyleタグを足します
index.vue<template> </template> <script lang="ts"> </script> <style lang="postcss" scoped> @import '@/assets/postcss/invite.css'; </style>Vuetifyをつかってレイアウトを作る
Vue.js Material Component Framework — Vuetify.js
今回作ったページはほぼほぼVuetifyの力をつかって調整していて、cssを使ったのは微調整ぐらいでした。
Vueでマテリアルデザインに沿ったレイアウトを作るためのいろんな機能を提供してくれるので
基本的にはタグを埋め込んで終わり。という感じです。一応元Android開発をしていたので、ToolbarとかSnackbarとかきいて何かわかりますが、聞き馴染みのない方には最初苦労するかもしれません(主に検索で)
比較的公式のドキュメントもまとまっている……と思います。ページのコンポーネント構造について
そもそも今回作ったコンポーネントの構造を雑に説明するとこんな感じです。
layouts/default.vue<template> <v-app> <nuxt/> </v-app> </template>上はおまじない的なやつです。vuetifyに欠かせない部分になります。
このテンプレートをベースに、index.vueはつくっていますindex.vue(概略)<template> <v-container> <!-- 下タブの選択によってアニメーションしながら切り替えるためのコンポーネント --> <v-window /> <v-snackbar /> <v-bottom-nav /> </v-container> </template>レスポンシブなページの対応はだいたいVuetifyがやってくれるのでcssはほぼ何もがんばりません。
また、v-bottom-nav
などは、ページに下部に固定できたり、カードや検索窓なども提供されているので、アプリっぽいwebページを作るにはとても強力なツールかなと思いました(雑感)コンポーネント紹介
v-window
Windows — Vuetify.js
ページ遷移のトランジションが簡単にできちゃうコンポーネント
<v-window v-model="activePage" :touchless="true"> <v-window-item :value="'home'"> <home/> </v-window-item> <v-window-item :value="'register'"> <register/> </v-window-item> <v-window-item :value="'other'"> <other/> </v-window-item> </v-window>デフォルトは、スワイプによる画面遷移が備わっていますが
調整する暇もないし、タブによるアクションのみにするため
touchless = true
としています。あとは上の通りですが、
activePage
に入ってる値によって出す表示を切り替えているだけです。
ページによって要素の高さが違うと思うので、遷移時に気になる場合はscroll位置の調整などもしてみてください。ロジックの分離について
今回の要件だけならVueだけでもよいのですが、NuxtでVuexが簡単に入れられるので
意識して実装してみました。公式の図からVuexにおけるフロー図です。
このwebページでは、ducksパターンでstoreを作成しています。
React + Reduxでもそうなのですが、action type
やaction
やreducer(Redux)
mutation(Vuex)
をそれぞれ分けると管理がつらいので、どうせそれぞれが密なものであれば一つのファイルでまとめよう。っていうデザインパターンです。あとは
- Vueでイベントがあれば、Actionを呼ぶ
- Actionごとの処理をする
- 処理後に状態の変更をcommitをしてstateに反映する
- Vue側で新しい状態を反映する
という流れにそって書きました。
それぞれが、役割以上のロジックを持たないようにしましたが
今回はそれとあわせて一つ意識していることについて書いておきます。「templateは状態の変え方をしらない」ようにする
Smart UIにならないようにしましょうというやつです。
Atomicデザインを目指したり目指さなかったりしても、可能な限りViewで表示以外のロジックを持つべきではないと思います。例えば Vueファイルのクラス内で、ボタンが押されたときに次のページに遷移するという処理のために
this.$store.dispatch('goToNextPage', current + 1)という書き方をしたこともあると思いますが、一度ここで立ち止まって考えてみましょう。
本当に次のページは「currentに1を足した値」でいいのか
「そりゃ1ページの次は2ページでしょ?」といえば普通なのですが、それはあくまで現実の話で
コードに落とし込んだときに、この書き方は「Viewが表示の変え方を+1することと知ってしまっている」状態になります。
もしかしたら、文字列で管理されているかもしれません。例えばですが、表示系からは「次のページへいく」という振る舞いだけ呼び出し
this.$store.dispatch('goToNextPage')そして、Actionで以下のように「次のページは、今のページ + 1である」という振る舞いの詳細を定義する。
goToNextPage(context: ActionContext<IState, any>, payload: any) { const nextPage = context.state.current + 1; ...という書き方で、可能な限りViewからロジックを剥がすことで
そのコンポーネントの再利用性が高まっていくと思います余談ですが、ReduxでContainer Componentで書いたとき、connectの第三引数の
mergeProps
を使うことで、テンプレート側に状態を渡さずに dispatchイベントで現在の状態を知る。といった書き方もできるので、「テンプレート側に値を渡してたぜ!」という方はぜひぜひ。import { connect } from 'react-redux' // 色々省略 export default connect( mapStateToProps, mapDispatchToProps, mergeProps // これ }(component)redux connect mergePropsとか調べれば色々出てくるかと思います!
react-redux/connect.md at master · reduxjs/react-reduxデプロイをする
最後の工程です。
index.htmlの出力 と s3のアップロードについて書いていきます。htmlの出力
今回はサーバまわりを書いてないですが
create-nuxt-app
で作っている場合、/pages
以下のvueファイルの名前にならってエントリポイントが作成されます。そして、
nuxt generate
コマンドはそのエントリでアクセスしたときに表示されるhtmlをファイルとして出力します。例/pages - index.vue - error.vue↓このようなファイルをおいている場合、上のコマンドを実行すると
dist/index.html dist/error.htmlというファイルと関連するcssやjsが出力されていると思います。
こちらをs3にアップロードしていきます。generateの結果をs3へアップロードする
とうとう最後の作業です。以下のようなコマンドをpackage.jsonのscriptに足しています。
package.json"upload": "aws s3 sync ./dist s3://${npm_package_config_bucket} --include \"*\" --acl public-read --cache-control \"max-age=900\" --profile=cli",ビルドする内容が複雑になりすぎて、scriptに書きたくない量になりそうなら
適宜いい感じにgulpとか使ってあげてください。
npm configの変数については後述しています。まず
aws s3 sync
コマンドで、指定のフォルダをまるっとs3の特定バケットにアップロードしています
aws s3 sync ./dist s3://${npm_package_config_bucket}
ソレ以外のオプションについては↓のとおりです。
option 説明 include excludeとあわせて使うオプションです。今回は不要で実はミスしていました acl s3においたコンテンツに対するACL設定です。 public-read
で全Userが参照可能になりますcache-control s3においたコンテンツにCache-Controlを設定します profile aws-cliにdefault以外のユーザーを設定してるとき、コマンドを実行するユーザー設定を指定します 不要なファイルを上げないようにexcludeといったオプションも細かく設定していくべきなときもありますが、今回は特に気にせず
nuxt generate
した結果を全部アップロードしています。これで、全てが完了です(たぶん…)
おまけ: npm configで公開したくない値をscript上で変数化する
ついでですが
npm config
を使ってgit上にバケット名を公開しないようにしています。
privateな場所であれば気にしなくてもいいですが、今回は最終的にpublicなリポジトリに公開をするため、npm config
でscript上で使われる値をセットできます。例として
package.json
に定義したname
がappname
の場合npm config set appname:bucket <値>とsetするとscriptで
"hoge": "echo ${npm_package_config_bucket}"のように
npm_package_config_<変数名>
形でscript上で展開することもできます。動作確認
ここまでやったすべての確認をしていきましょう。
- 作成したドメインでアクセスができた
- 作成したドメインのルートにアクセスしたら index.htmlにアクセスするようになっているので、s3にデプロイしたindex.htmlが表示された
- 初回アクセスで、userIdを取得するために /inviteesのパスにリクエストして、レスポンスが返ってきた
- 入力フォームに値を入力してOKを押したら登録成功した
- DynamoDBに登録した情報が表示されている
備忘録的に順にかいてますが、3月前の記憶がベースなのでもしかしたら足りない情報があるかもしれません…
もし失敗してCloudFrontにキャッシュ設定をしたせいでs3を更新しても反映されない。
といった場合、CloudFrontの画面からInvalidation
のタブを選択して、Create Invalidation
のボタンを押すとパスを入力する画面が表示されると思います。こちらの入力欄に
/*
と入れてInvalidate
を実行すると まるっとキャッシュを消してくれます(実行完了に少しだけ時間がかかります)
注意 やりすぎると課金にかかわるので、やりすぎないかお財布と相談してくださいまとめ
言い訳になりますが、2019/2末ぐらいには開発完了していてそれを出してから放置をしていたため、この備忘録も当時の記憶を頼りに書いてるのでだいぶヌケモレあるんじゃないかな…と思います。
フロントにいたっては、あまり特殊実装もしてないのであとは「コード見て」状態になってますね…/(^o^)\多分おかしいところあるきがするのでアレば教えてください
とりあえず整理もせずひたすら書いたのでクソ長いですが、
もしここまで読んでくださった方がいたら本当にありがとうございました!会社だと整った環境があるので、なかなか1から作ることはないからこそ
お勉強としてやってみましたが…それでもAWSで完結するんだから楽ですよねパーティも終わってこれも書き終わったのでAWSの解約をして
趣味のゲームづくり(Unity)に力を注いでいきたいなという そんな今日このごろ