20190627のvue.jsに関する記事は15件です。

Vue・VueCLIのバージョンが分からん!

何なんですかこれは!

vuenpm.jpg

Vueのバージョン確認をしようと思い、ググったら色んなコマンドが出てきたので試したら全部バージョンが異なるじゃないですか!何ですかこれは!(npm初心者)

答え合わせ

npm vue --version

?これはタダのnpm本体のバージョンでした

vue --version

?VueCLIのバージョン(コマンドが"vue"だからそりゃそうだ!)

npm list vue

?これが純粋なVueのバージョン!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

はじめてのvue-property-decorator

この記事はVueを勉強している段階からTypeScriptでクラスベースのVueアプリを作りたい!という方へ向けて例を交えながらvue-property-decoratorの機能を基本応用上級の3セクションに分けて説明していきます。

基本では、Vueでアプリを作る上で必須となる機能を、応用では用意されている便利なデコレータを、上級では普通の開発ではほぼ使わない機能について説明します。
とりあえずは基本のみ理解しておけば困ることはないでしょう。

動作確認バージョン

vue-property-decorator v8.1.1

基本

コンポーネントの定義

@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-decoratorVueクラスを継承することを忘れないように気をつけてください。

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のオブジェクトを指定することができます。

以降で各種デコレータを紹介しますが、そこで定義できないcomponentsfiltersmixinsなどのプロパティは@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に併せて定義できます。

Vue.js公式ドキュメント#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というdatamethodsは定義できません。

@Provide / @Inject

Vueでは親でprovideとして定義した値をその子要素(親子でなくても良い)からinjectで参照することができます。

Vue.js公式ドキュメント#inject/provide

以下の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のサンプルコードではreadonlyprop!: 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>

より強固に型安全にしたい場合はこれらに気をつけて開発するようにしてみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue-simple-suggestで簡単にテキスト入力候補をリストで表示

はじめに

正確な名前が分からなかったり、検索項目が多い場合サジェストは非常にUXの質を高めてくれます。

今回はTypescriptとVueで簡単にサジェストの表示を実装したのでその記録を残しておきます。

今回利用するのがこちらです。

vue-simple-suggest

vue-simple-suggestのインストール

まずはvue-simple-suggestをインストールしましょう。

yarn add vue-simple-suggest

これでインストール完了しました。

これだけだとインポートしたときに方エラーが出てしまいます。型の定義ファイルにvue-simple-suggestを宣言しておきましょう

shims-vue.d.ts
declare 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を向上させるには必須ので機能です。

このプラグインを活用して爆速でサジェストを実装しましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jestの結果がおかしくなったら--clearCacheしよう。

ある日の出来事...

.vueでレイアウトちょっと変えただけだけどいちおうテストしとこうっと。

やぁ!

yarn run test:unit

結果はどうかな?

pre.png

...なんかめちゃめちゃやんけ!!

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

aft.png

ばっちりですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptとElmを比べてみた〜後編・Vue.jsとも比べてみた〜

※前編はこちらやで。

ハスケル子「引き続き、JavaScriptElm・・・」
ハスケル子「そして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>

ワイ「見たままやな」
ワイ「ボタンをクリックするとincrementFuncdecrementFuncという」
ワイ「関数が実行されんねんな」
ワイ「関数は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でいうstorestateみたいなもんですね」
ハスケル子「要はmodelイコール状態です」
ハスケル子「そのmodelの中のint1増加させます」
ハスケル子「メッセージがDecrementだった場合は1減らす感じですね」
ハスケル子「そして、それによって生成された新しいmodelを戻り値として返すって感じです」

ワイ「ほえ〜、新しいmodel、つまり新しい状態を返すと」
ワイ「それがリアクティブにビューに反映されるっていうこと?」

ハスケル子「そうです」

ワイ「そうなんやね〜」
ワイ「Elmって、VueとかReactみたいに仮想DOMを内蔵してたんやね」
ワイ「あとVuexやRedux相当の機能もか」

ハスケル子「そうです」
ハスケル子「っていうかVuexReduxも、Elmの影響を受けてます」
ハスケル子「Elmというか、今みたいなThe Elm Architectureというパターンの影響ですね」
ハスケル子「しかもElmはとってもシンプルなので」
ハスケル子「学習コストが低くて、やめ太郎さんにピッタリです」

ワイ「どういう意味やねん

ハスケル子「オンラインエディタでさっきのカウンタのサンプルコードを色々いじってみると」
ハスケル子「更に分かると思いますよ」

ワイ「やってみるわ」
ワイ「おおきにやで、ハスケル子ちゃん」

ハスケル子「Vue勉強しててもReact勉強してても」
ハスケル子「副作用を起こさないように、とか」
ハスケル子「外部の状態に依存しない純粋な関数・・・つまり参照透過的な関数を書こう、とか」
ハスケル子「色んなドキュメントに書いてあるんですよ」
ハスケル子「じゃあ、そういう風にしか書けないElmやりゃあいいんですよ

ワイ「お、おう・・・」
ワイ「前向きに検討するわ・・・
ワイ「っていうか、今回JSやVueと比較したおかげでElmの文法がかなり分かったから」
ワイ「普通に読み書きできそうやな・・・」
ワイ「やってみるで!

〜おしまい〜


  1. 配列みたいなやつやで。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

既存の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.js
module.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-vue

jest.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のファイルが格納される。

参考

Snapshot Testing

カバレッジを表示させたい場合

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

参考

Vue Test Utils + Jest でVue.jsの単体テストを行う
Configuring Jest

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初心者向け]VuexのStoreを細かくモジュール分けしよう

はじめまして、PMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います。

はじめに

Vuexを始めたばかりの自分はVuexに関連する処理をすべてstore.jsなどの一つのファイルに記述してしまっており、ファイルが非常にファットになってしまっおり、可読性も非常に下がってしまっていました。
そこでVuexファイルを細かくモジュール分していきたいとおもいます。

必要な環境

  • Vue
  • Vuex

実装

まずstore/以下に任意のjsファイルを作成してください。今回はuser.jsというファイルを作成しています。

user.js
import 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.js
import 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.js
import 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のファイルを細かくモジュール分けすることができました。
ファイルを細かくモジュール分けすると可読性が上がり、開発効率が上がると思います。
機会があればぜひ試してみてください。
なにか間違いなどあれば指摘していただけると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[vue] スマホ重視な数字を選択できるコンポーネント

junban.appで作成したコンポーネントを公開していこうと思います。

ちゃんとしたコンポーネント配信が目的ではなく、プロジェクト毎にコピペして、文字とかアイコンとかはその都度カスタマイズを目的としてますので、ここslotにした方が柔軟性が~とかは考えないです。

機能概要

数字を選択できるコンポーネントです。
スマホで数字入力(input[type=number]とか)はスマホのキーボードが表示されて嫌なので作りました。
movie_20190627_122641.gif

テスト環境

  • "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()とかでバリデーションできます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptとElmを比べてみた〜前編〜

Elmとは、JavaScriptにコンパイルできる言語、いわゆるaltJSです。

変数宣言(のようなもの)

JavaScriptの場合

const a = 1;
let b = 1;
var c = 1;

ワイ「const定数いうて再代入できひんやつやな」

Elmの場合

a = 1

ワイ「constletvarも無いねんな」

ハスケル子「はい」
ハスケル子「デフォルトで再代入不可です」
ハスケル子「不変なので、変数ですらなくて」
ハスケル子「ただ値に命名している、値を定義しているって感じですね」
ハスケル子「あとセミコロンも要りません」

ワイ「再代入はできなくても、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関数に対して、add35という」
ハスケル子「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;

ハスケル子「───こんな感じで」
ハスケル子「abに値を代入するより上の行で」
ハスケル子「abを使った計算などをしようとすると」
ハスケル子「エラーになっちゃいますよね」

ワイ「なるほどな」
ワイ「でもまあ、それは普通そうやろ」

ハスケル子「それがElmの場合は違うんですよ」

Elmの場合

c = a + b
a = 3
b = 5

ハスケル子「↑これも普通にOKです

ワイ「ええ・・・」
ワイ「ab定義するより上の行で」
ワイ「abを計算に使ってるやん・・・」

ハスケル子「はい」
ハスケル子「関数と同じ感じなんですよ」

ワイ「ああ・・・」
ワイ「JSでも関数はそうやもんな」
ワイ「下の方で宣言した関数、上の方で使えるもんな」

ハスケル子「はい」
ハスケル子「Elmでは全ての値が不変なので、それが可能なんです」

ワイ「全ての値が不変・・・つまり再代入という概念が存在しないということやろ?」
ワイ「それやと順番が関係なくなるの?」
ワイ「なんで・・・?」

haskellko2.jpeg

ハスケル子「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状態が変わることが無いから」
ワイ「a5に変えた後に呼び出したらどうこう・・・」
ワイ「みたいな話がそもそもあり得へんわけやな」

ハスケル子「そうです」
ハスケル子「時間が止まってるようなものなので」
ハスケル子「コードの中に前とか後とか無いイメージです」

ワイ「なるほどなー」
ワイ「時が止まってるような」
ワイ「1フレームの中で全てが実行されているような」
ワイ「不思議な感じやな」
ワイ「スタープラチナ・ザ・ワールドみたいやな」

ハスケル子「オラオラオラオラ!って感じです」

ワイ「痛い!痛い!
ワイ「やめてぇや」

ワイ「ええと、つまり」
ワイ「時間が止まっとるようなもんやから」
ワイ「値の定義より上の行で、その値を使っても」
ワイ「問題ないんやな」

ハスケル子「そんな感じです」

ワイ「でも、なんの値も変えられなくて」
ワイ「この言語、何ができるの・・・?」

ハスケル子「普通にシングルページアプリケーションとか」
ハスケル子「動きのあるゲームだって作れますよ」

ワイ「えぇ・・・!?
ワイ「全て不変のはずやのにボール動いてますやん・・・!
ワイ「言うてることちゃいますやん・・・!」

ハスケル子「気になるならElm Guide読んでみてください」

ハスケル子「じゃあ」
ハスケル子「次はオブジェクトとかイベントハンドラについて比べてみましょう」

〜後編に続く〜


  1. 詳しくは後編にて。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[vue] 配列の中に文字列を入れたり消したりするコンポーネント

junban.appで作成したコンポーネントを公開していこうと思います。

ちゃんとしたコンポーネント配信が目的ではなく、プロジェクト毎にコピペして、文字とかアイコンとかはその都度カスタマイズを目的としてますので、ここslotにした方が柔軟性が~とかは考えないです。

機能概要

フォーム入力バインディングした配列の中に文字列を入れたり消したりするコンポーネントです。

movie_20190627_105607.gif

テスト環境

  • "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が貼れたら解決しそう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[vue] フォーム入力バインディングした配列の中に文字列を入れたり消したりするコンポーネント

junban.appで作成したコンポーネントを公開していこうと思います。

ちゃんとしたコンポーネント配信が目的ではなく、プロジェクト毎にコピペして、文字とかアイコンとかはその都度カスタマイズを目的としてますので、ここslotにした方が柔軟性が~とかは考えないです。

機能概要

フォーム入力バインディングした配列の中に文字列を入れたり消したりするコンポーネントです。

movie_20190627_105607.gif

テスト環境

  • "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が貼れたら解決しそう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.js
import 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.js
import 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
          }
        }
      }
    }
  }
})

確認してみる

ここで以下のようにメニューバーが描画されていれば成功です。
スクリーンショット 2019-06-27 1.30.51.png

動きを付けていく

この状態ではハンバーガーメニューを押しても何も反応しません。
ここからは動きを付けていきたいと思います。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を使っているので少し難しく見えるかもしれませんがぜひチャレンジしてみてください。
間違いなどあれば指摘していただけると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】Vue.jsをNetlifyにホスティングするときのリダイレクト設定

※自分用。SPA勉強中の初心者。

経緯

vue.jsでのフロント側でのルーティング設定で、vue-routerを用いていたが、Netlifyにデプロイして、トップ以外のページのURLを叩いた時に404になってしまい、困ったので調べた。

調べた

公式に書いてあった。

サーバーの設定例 にあるように、設定ファイル等ケースにより対応が必要らしい。

Redirects | Netlifyをみると、_redirects というファイルをindex.htmlと同階層に設置すればいいらしい。

_redirects
/* /index.html 200

上記で問題なく、想定通りのルーティングで動作を確認した。

また、静的ファイル置き場の public ディレクトリの中に_redirectsを入れておくことで、デプロイ時の自動ビルドでも問題なくそのまま吐き出されていた。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.git

2. Laradockの.envファイルの編集

laradockディレクトリが出来上がるので、その配下のenv-exampleファイルをコピーし、.envファイルを作成します。

$ cd laradock
laradock $ cp env-example .env

laradock/.envの以下2つの設定を編集します。

laradock/.env
APP_CODE_PATH_HOST=../vuesplash

DATA_PATH_HOST=../data

3. PostgreSQLのバージョンの指定

チュートリアルではPostgreSQLのバージョン指定はありませんが、9.6.12を使用することにします。
PostgreSQLのDockerファイルを以下の通り編集します。

laradock/postgres/Dockerfile
FROM postgres:9.6.12

LABEL maintainer="Ben M <git@bmagg.com>"

CMD ["postgres"]

EXPOSE 5432

4. コンテナのビルド

コンテナをビルドします。

laradock $ docker-compose up -d --build workspace postgres php-fpm nginx

5. 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/.env
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=sample
DB_USERNAME=default
DB_PASSWORD=secret

7. データベースを作成する

まず、PostgreSQLに接続します。

laradock $ docker-compose exec workspace psql -U default -h postgres

パスワードを入力します。

Password for user default: 

続いて、データベースを作成します。
データベース名は、チュートリアルで指定されているvuesplashにします。

default=# create database vuesplash;

PostgreSQLとの接続を終了します。

default=# \q

8. 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です。

最後に

以上です。

本記事が、この素晴らしいチュートリアルに取り組む方のお役に立てば幸いです:wink:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsとAWSで招待状webページを作ったまとめ

概要

2018年に結婚しました!ので!
ここはエンジニアらしくパーティ招待状Webページをつくってみることにしたのが始まりです。

とりあえず無事にパーティも終わったので、開発時の記憶をさかのぼりながら残す備忘録ですが
申請から開発まで一通り殴り書くので、何かしら参考になれば幸いです。┗(^o^)┛
…ちょっと前の記憶を掘り起こしながらやるので、間違って書いてそうなところもある気がしますが温かい目で御覧ください :bow:

開発環境はMacなので、Winの方は適宜読み替えていただけると幸いです :pray:
AWS上で日本語表示できている部分は、日本語の画面で説明してる…はずです

成果物

webページ

ざっくり3つの画面構成です。(3つ目はフリー素材やOSSの情報なので割愛)
GoogleMapや開催日を記載したホーム画面と、実際に参加者の情報を登録してもらう登録画面の2つです。

ホーム画面 登録画面
スクリーンショット 2019-03-06 13.52.42.png スクリーンショット 2019-03-06 15.38.07.png

システム構成

今回のwebページのシステム概要はこんな感じ

Route53でドメインをとったうえで、「必要なときに必要な程度稼働してくれる」実運用を考えながら、今回そこまでやる必要はないシステムを作ってます。先人たちの知恵借りまくりです。

Github

書いたコードは、以下の2つ。

システム リンク
webページ https://github.com/tyabata/web-invite
api(lambda) https://github.com/tyabata/lambda-api-invite

個人情報とかcommitに入れちゃったのでgitのcommitログだけ消し去ってます :innocent:

採用した技術

関連キーワードの羅列。詳細は次項から説明します。

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.0

「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を使う」という流れ

  1. Route53でドメイン取得 -> Certificate ManagerでSSL証明書取得
  2. s3準備 (linkのみ紹介)
  3. aws-cliの導入
  4. Lambda準備からDynamoDBにデータ登録まで
  5. API GatewayLambdaを接続
  6. CloudFrontS3API Gatewayのマルチオリジンにバックポストする際の設定

必要なとき以外、見る必要も見られることもない招待ページなので
コンピューティング時間を減らして省エネ運用の構成をとって…いるように見せかけてやたら色々準備したのは勉強がてら実際に使うことを考えた構成を目指してみたという具合です。

Client Side

Nuxt.jsを利用してページを開発 -> index.htmlを出力してS3にアップロードするまで
を以下の手順で説明していきます。

  1. nuxt-tsで開発できる下準備
  2. ページを作る
  3. index.htmlを出力する
  4. S3にアップロードする

開発詳細 : Server Side

ここからが本題です

ドメイン取得と証明書作成まで

Route53でドメイン取得

まずはドメインを取得します。
新しいドメインの登録 - Amazon Route 53

画像は省きますが、基本は同じ。
リンク先の説明にならって作業をすれば、自分のドメインがつくれます。
トップドメインによってお値段が異なるので、今回は安価でよくみる .netを選択

  • トップドメイン以降の自分でほしい名前を入力
  • チェックの結果がOKであれば、それをカートに入れる (Add to cart)
  • 連絡先などを入力して進むと登録完了
  • 登録したドメインが [Domain registration in progress]の状態からしばらく待つと[All Contacts]になれば完了。SOAレコードとかも同時に作成済みの状態になります。

ドメイン作成はここまでですが、これだけは登録したタイミングで課金が発生します。

Certificate Managerで証明書作成

さすがにwebエンジニアとしてhttpのリンクで友人だけでなく、嫁の知人含めて招待ページ登録してねー。と公開するのは社会の窓全開でご挨拶してる気がするのでサボらずちゃんと作成します。

初回は
[Provision certificates(証明書のプロビジョニング)] => [Get Started(今すぐ始める)] => [Request a Certificated(署名書のリクエスト)]
と選択していくと以下のような画面になると思います。

image.png

ドメイン名の入力欄に先程登録したドメインを入れましょう。
ここではワイルドカード証明書のリクエストもできるので、私は *.hoge.netのような名前で証明書を作成しました。
この後、進めていくとドメイン所有者(つまり自分)に下記のようなメールが飛びます。
image.png

DomainやAccountIDや取得したRegionなどに問題がなければ、メールに記載されている
To approve this request, go to~と書いてあるあたりのリンクから遷移して承認完了させます。
下記のような画面まで行けば、証明書の作成まで完了です。
image.png

実際に証明書を設定したりするのはCloudFrontあたりを扱う項へ。

S3の準備

ほか項目含めて全部書くと、やたら長いドキュメントになるので備忘録としてリンクだけ。
S3 バケットを作成する方法 - Amazon Simple Storage Service

バケット作成後に追加で設定したものは

  • バージョニングの有効化 + ライフサイクルから旧バージョンに対する削除の設定
    • 本番リリース後に問題発覚して戻すことがある場合、バージョン指定で戻せる
    • しかし一度動いてしまえば1日以上たっても変わらない
  • cliコマンドからのデプロイするために対象アカウントのみ書き込みを有効化
  • それ以外の全ユーザーに対してはReadのみ有効

ライフサイクルの設定については、作成したバケットの上部にある「管理」タブから
[ライフサイクルルールの追加]という項目からできます。
上記通り、一時的なロールバックを考慮してバージョニングの有効化をしたので
一日以上たった過去バージョンは削除する。というライフサイクルを設定しました。
(結果的に不要でした)

image.png

アクセス権限については、CloudFrontからアクセスが前提なので
本当は全ユーザー有効設定ではなくCloudFrontからのアクセスに対してReadを与えるような設定が良いと思います。
…色々調べながらやってたので、全ユーザーがreadできる方が都合がよかったのです… :bow: w
(といってもさすがにURLは公開してないです)

とりあえずこのタイミングでは、表示確認のためのindex.htmlに適当になんか書いたものをバケットのrootにおいといてください。

設定についてはこちらを参考にしてみてください。
CloudFront ディストリビューションからのみ S3 バケットへのアクセスを許可する

アクセス権限は適切にね!!

aws-cliの準備

Lambdaやs3にデプロイをするために、AWS用のコマンドラインツールであるaws-cliからデプロイする準備をします。

IAMでユーザーの作成

aws-cliでアクセスする際に使うユーザー設定を行います。
admin使ってもできるんですが、お勉強とお作法的に分けます。

下記リンク等を参考に必要な情報を作成します。
最初の IAM 管理者のユーザーおよびグループの作成 - AWS Identity and Access Management

IAMから、左カラムのナビゲーションメニューから ユーザーを選択し、上部にあるユーザーを追加を選択します。
すると下の画像のように、ユーザーを追加するための設定画面が出てくるので、下の表のように設定していってください。
image.png

項目 設定 補足
ユーザー名 cli ※なんでもOK
アクセスの種類 プログラムによるアクセス
グループの作成 あとで説明 既存のポリシーを直接アタッチすることもできます
タグ なし IAMの管理用です。個人開発で特にいらないので今回は省略

ここまで入力すると、確認の表示が出てくるので問題なければ次に進むと以下のようにアクセスキーとシークレットアクセスキーが取得できます。
どこかにメモっておきましょう。あとのcli設定に使います

image.png

IAMの概念(雑まとめ

グループ作成について説明をしていきます。
AWSに初めて触ると ロール ユーザー グループ ポリシーといろんな言葉がでてきて
チンプンカンプンになる(な気もしてる)ので、私の雑まとめです。

  • ポリシーは 「AというポリシーはDynamoへのRead権限をもつ」といったルール的なもの
  • ユーザー、グループ、ロールはそれぞれポリシーをアタッチして使う
  • ユーザーやグループは人の管理に使う
  • ロールはシステムで使う

という雰囲気理解です。とりあえず関係性が雰囲気でもわかればOKかと。

上の画像で線を足し忘れましたが、ユーザーにポリシーをアタッチできます。
が、複数で開発する場合は「開発」というグループをつくってユーザーをそこに紐づけていくことで、わざわざ個別にポリシーをアタッチする手間を一つにまとめられる利点等があると思います。

  • Lambdaにデプロイするには AWSLambdaFullAccessポリシー
  • S3にデプロイするには AmazonS3FullAccessポリシー

といった具合で今回は

項目 設定値
ユーザー名 cli
グループ名 deployment
ポリシー(グループに対して) AWSLambdaFullAccess,AmazonS3FullAccess

を設定しています。

「勉強だしとりあえず全権限ふっておけ」といった場合は
AdministratorAccessを設定すればOKです。
仕事でやるなら用法用量はまもりまs(ry

aws-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 IDAWS Secret Access Keyは先程取得した値を設定してください。
regionはとりあえずTokyo(ap-northeast-1)とかでもいいと思います。
Outputは jsonTextとかありますが、これはご随意に(説明略)

[~] 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-invite

APIのロジック

  • Lambdaに対してリクエスト
    • /invitees/userへのGET と /inviteesへのPUTを処理する
    • 実行例外は拾って、500を返すようにしている
  • /invitees/user : GET
    • ページを初めて開いたときにアクセスされる。サーバ側でもつパスワードでcryptoした文字列をUserIDとして返す。UUIDはcrypto-jsを利用して時間から生成してる
    • ページ側がlocalstorageで保存しているので、消さない限りは再リクエストは発生しない(が、消されると新しいユーザーとして再度UUIDを発行します)
    • webページ用にサーバを建てないようにした結果。こうなった :innocent:
  • /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とかは古いので適当に置き換えてください :bow:
アタッチしている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がポリシーとしてアタッチされている状態であれば、これで新規登録や更新が完了します。

image.png
Lambdaで登録したregionで開くと、上の通りに登録されていることが確認できると思います。

そしてLambdaにアタッチしているroleですが、 CloudWatchにログを流す DynamoにアクセスしてR/Wするといったポリシーをふったroleをアタッチしています。

詳細説明は省きます :bow:
ただ、Lambdaのログを流すために設定しておくことで、問題があったときにも気づけるので登録しておきましょう。
Amazon CloudWatch Logs とは - Amazon CloudWatch Logs

DynamoDBへ登録

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のテストを使って、試しに処理を実行させることは可能です。

image.png
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にあわせて設定して保存を押すと作成完了です。

image.png

リクエスト/レスポンスの詳細設定

ここから、API Gatewayの詳細を設定していきます。
今回は作ったもののうち、PUT : /inviteesについて説明していきます

項目 概要
メソッドリクエスト 作成したタイミングの値が入ってる。今回はこのまま
総合リクエスト リクエスト情報の制御的なところ。マッピングテンプレートをいじります(後ほど)
Lambda 設定されているLambda functionの名前が表示されていればOK
総合レスポンス 返すレスポンスを制御するところ。ここに400や500のパターンを追加する(後ほど)
メソッドレスポンス こちらも200, 400, 500の3つを追加しておく

リクエストのマッピングについて

コレはパッと身よくわからなくてつらかったので、ここでは概要だけ。
詳細はリファレンスや先人の知恵をおかりするのをオススメします。
API Gateway マッピングテンプレートとアクセスのログ記録の変数リファレンス - Amazon API Gateway

上記で設定した項目のうち 総合リクエストのを選択し、下部にあるマッピングテンプレートの項目を選びます。

「テンプレートが定義されていない場合」を選択し、Content-Typeapplication/jsonを選びます。

image.png

追加すると、さらに下にテンプレートを入力する欄が表示されるので、今回は下のように入力しました。

{
    "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
    }
}

とりあえずわからなくてもいいです。私もドキュメントにならった説明しかできなないです。
拡張したくなったらリファレンスを読みましょう :innocent:
雑にいうとAPI Gatewayで受けたリクエストを展開してLambdaのコードで受けやすい形に変換してるといったところになると思います。

上のようにテンプレートにはめ込んだものを handlerの第一引数で受け取って処理を実行しています。
https://github.com/tyabata/lambda-api-invite/blob/master/src/index.js#L22

レスポンスのマッピングについて

Lambdaがいくらエラーメッセージを返しても、API Gatewayは200を返してしまいます。
そこで、特定の文字列が含まれるときは 400500で返せるように設定します。

統合レスポンスを選択します
スクリーンショット 2019-06-23 18.04.22.png

ココらへんはシンプルに
.*"status" *: *400.*という文字列がレスポンスに含まれていれば 400を返す
といった設定です。500も同様。

次に一つページをもどって、全体の画面からメソッドレスポンスを選択し、HTTPのステータスという項目に400や500などを追加します。
これで、リクエストに対して適切なステータスコードを返せるようになります。

APIのデプロイ

忘れがちですが、これをやらないと反映されないので、メソッド作成等と同様にアクションから APIのデプロイを押しましょう。
デプロイ対象のステージが表示されるので、選択してデプロイを押すことで初めて反映されます。

image.png

…ステージの説明を書き忘れていましたが、コンソールの左にAPIごとにステージという項目があり
ソレを選択して、 betaとかprodとかステージを分けておくと、開発環境と本番環境を分けておくことができます。

例えば betaステージの場合は、下のようなパスになります。
https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/beta

スクリーンショット 2019-06-23 19.31.37.png
このURLはCloud Frontの設定でも使います。

動作確認

最後に動作確認として、メソッドの実行画面の左側にある 雷マークがついたテストを選択してテストを行います。
メソッドテストと表示されている画面に遷移したら、一番したのリクエスト本文の項目に
リクエストするbodyの情報をセットします。

右側に実行時のログが表示されます。ただしく設定ができていれば、成功時のレスポンス内容がログに表示されると思います。

CloudFrontでS3とAPI Gatewayにバックポストする

やっとServer Sideの説明の最後に来ました…! 総集編?です。

取得したドメインを設定したCloud Frontでリクエストを受け取って、s3からhtmlを返すか、APIにリクエストするかをパスによってバックポストする設定を追加していきます。

2つのオリジンの追加

  • Cloud Frontを開いて、Create Distributionから作成
  • WebGet 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 PolicyHTTPS 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の設定を足していきます

image.png

  • Create Originを選択
  • Origin Domain Name でs3のオリジンを選択する(サジェストで表示されます)
  • Origin Access IdentityCreate a New Identiyを選択する。(理想 : こうすることで、s3へのアクセスをcloud frontのみにできる。が、今回はやってないです)

これでs3のオリジンも追加しました。

アクセスしたパスでバックポストするオリジンを分ける

次に Origins and Origin Groupsの隣の Behaviorsタブを選択して
Create behaviorを追加します。

今回は、jsやcssなどをs3に置くので API以外のパスはすべてs3に流れるように設定します

で設定したのが以下の2つのパス

  • Default(*) : S3にバックポストする
    • Viewer Protocol PolicyRedirect HTTP to HTTPS
    • Allowed HTTP MethodsGET 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使ってる感がモロバレなので、設定をします。

image.png

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の結果が返ってくるようになるはずです。

…もし動かなかったらごめんなさい。 :bow:
ググれば多分解決できる程度に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/6d99fbbdde76b84b6c8baa4d1c7b73f3a5683afa

Nuxtと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 yarn

TypeScript対応はここから下を入れてjsをtsに入れ替えていく作業になると思います。

yarn add ts-node @nuxt/typescript

簡単なページはこれで完結できるのですが
webpackがラップされてる感じなのでアレコレ拡張するのが煩わしく感じることもあると思うので、プロダクトにあわせて使う使わないは判断すれば良いかなと思います。

ページを作る

といっても、基本はVueとVuexです。

  • Nuxtにのっとって書く
  • axiosで用意したAPIと通信をする
  • Vuetifyで基本的なデザイン作成
  • ロジックの分離を意識する

といったぐらいのことしか…やってないかも :thinking:
とりあえず書きます!

create-nuxt-appから少しだけ設定を変える

create-nuxt-appをつくると、ルートにstorepageといったフォルダができます。
ちょっぱやで作る分にはいいのですが、気になる人(自分含む)は
例えばsrcというフォルダ以下に移したい場合はsrcDirというフィールドに設定します。

nuxt.config.ts
const 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.ts
  build: {
    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は違う気がするな :thinking:

ちなみに テンプレの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
ページ遷移のトランジションが簡単にできちゃうコンポーネント
saaaample.gif

    <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 とは何か? | Vuex

image.png

公式の図からVuexにおけるフロー図です。
このwebページでは、ducksパターンでstoreを作成しています。
React + Reduxでもそうなのですが、action typeactionreducer(Redux) mutation(Vuex)をそれぞれ分けると管理がつらいので、どうせそれぞれが密なものであれば一つのファイルでまとめよう。っていうデザインパターンです。

erikras/ducks-modular-redux: A proposal for bundling reducers, action types and actions when using Redux

あとは

  • 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からロジックを剥がすことで
そのコンポーネントの再利用性が高まっていくと思います :thumbsup:

余談ですが、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に定義したnameappnameの場合

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のボタンを押すとパスを入力する画面が表示されると思います。

image.png

こちらの入力欄に/*と入れてInvalidateを実行すると まるっとキャッシュを消してくれます(実行完了に少しだけ時間がかかります)
注意 やりすぎると課金にかかわるので、やりすぎないかお財布と相談してください :bow:

まとめ

言い訳になりますが、2019/2末ぐらいには開発完了していてそれを出してから放置をしていたため、この備忘録も当時の記憶を頼りに書いてるのでだいぶヌケモレあるんじゃないかな…と思います。
フロントにいたっては、あまり特殊実装もしてないのであとは「コード見て」状態になってますね…/(^o^)\

多分おかしいところあるきがするのでアレば教えてください :bow:

とりあえず整理もせずひたすら書いたのでクソ長いですが、
もしここまで読んでくださった方がいたら本当にありがとうございました!

会社だと整った環境があるので、なかなか1から作ることはないからこそ
お勉強としてやってみましたが…それでもAWSで完結するんだから楽ですよね :innocent:

パーティも終わってこれも書き終わったのでAWSの解約をして
趣味のゲームづくり(Unity)に力を注いでいきたいなという そんな今日このごろ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む