20191112のvue.jsに関する記事は9件です。

laravelのbladeをVue.jsで書き換える【Form編】

はじめに

プログラミング学習をはじめ、10カ月です。
現在はLravelとVue.jsを学習しています。

この組み合わせだと世間的には、SPAと決まっているのですか?

Vueも少しずつわかってきたので、laravelのbladeをVueに書き替えて、MPAを作り始めました。

基本のCRUD処理をと思いましたが、さっそくつまずきました。

register画面を作ろう

laravelやVue.jsの基本は理解している前提で進めていきます。

ポイントを押さえながら解説します

まずは基本となるregister.phpです。

register.php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="csrf-token" content="{{ csrf_token() }}"> //①
  //中略(cssなど)
    <title>title</title>
</head>
<body>
    <div id="app">
        <registerform-component 
            :old="{{ json_encode(Session::getOldInput()) }}" //②
            :errors= "{{ $errors }}"> //③
        </registerform-component>
    </div>
    <script src="{{ mix('js/app.js') }}"></script>
</body>

htmlではポイントは3つ
①laravelではcsrf対策を取らないといけません。
headにトークンを仕込んでおきます

②フォームで重要なのは入力ミスがあった場合、元のフォームにリダイレクトされます。入力した内容は残って欲しいですよね。
laravelのbladeでは、oldヘルパーが使えたのですが、Vueでは使えませんでした。
ここはあとで解説します。

③入力ミスがあった場合、何が間違っているのか、バリデーションによるエラーメッセージが表示されます。
laravelでは$errorsでどこからでも拾ってこれるそうですね。

\$errors変数はwebミドルウェアグループに所属する、Illuminate\View\Middleware\ShareErrorsFromSessionミドルウェアによりビューに結合されます。このミドルウェアが適用される場合は、いつでもビューの中で\$errors変数が使えます。$errors変数はいつでも定義済みであると想定でき、安心して使えます。
https://readouble.com/laravel/5.5/ja/validation.html

しかし、これもVueの中では素直に使えませんでした。

Componentを作ろう

今回はRegisterForm.Vueを作成しました。
予め使えるようコンポーネントの登録はしておいてください。

RegisterForm.Vue
<template>
    <form action="/register" method="POST"> //④

        <input type="hidden" name="_token" :value="csrf"> //⑤

        <div class="form-group">
            <label>名前</label>
            <strong class="error" v-for="value in error.name">{{ value }}</strong> //
            <input class="form-control" name="name" type="text" v-model="name"> //⑦
        </div>
        <div class="form-group">
            <label>Email</label>
            <strong class="error" v-for="value in error.email">{{ value }}</strong>
            <input class="form-control" name="email" type="text" v-model="email">
        </div>
        <div class="form-group">
            <label>パスワード</label>
            <strong class="error" v-for="value in error.password">{{ value }}</strong>
            <input class="form-control" name="password" type="password"  v-model="password" autocomplete="off">
        </div>
        <div class="form-group">
            <label>再入力</label>
            <input class="form-control" name="password_confirmation" type="password" v-model="password_confirmation" autocomplete="off">
        </div>

        <input type="submit"  class="button" value="登録"> //⑧
    </form>
</template>

<script> //⑨
    export default {
        props:[
            'old',
            'errors'
        ],
        data:function(){
            return{
                csrf: document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
                name: this.old.name,
                email:this.old.email,
                password:'',
                password_confirmation:'',
                error:{
                    name:this.errors.name,
                    email:this.errors.email,
                    password:this.errors.password,
                }
            }
        },
    }

コンポーネントはこんな感じにしました。

④register登録は/registerにPOSTします。

⑤laravelではPOSTデータにcsrfトークンを仕込まないと、弾かれてしまいます。
scriptタグで取得したトークンをもたせます。
csrf: document.querySelector('meta[name="csrf-token"]').getAttribute('content')

これは、このコンポーネントが入るregister.phpのheadに記述したものをとってきています。

⑥ここはエラー文ですね。後述します。

⑦はVueの基本の双方向バインディングですね。
Vーmodelでinputが変更されると、dataも変更されます。

⑧でサブミットされますね。

先述の
<registerform-component
:old="{{ json_encode(Session::getOldInput()) }}" //②
:errors= "{{ $errors }}"> //③
</registerform-component>

の③で
コンポーネントに変数$errorsを渡します。
コンポーネント内ではpropで使用することができます。

同様に②で入力したデータはセッションに保存されています。
これをgetOldInput()で取得できます。
これをjson_encode()でjsonデータに直してコンポーネントに渡しています。

propで受け取ったデータは、thisで使用できます。
dataの初期値にthisで取得したデータを入れることで、入力したデータやエラーメッセージを表示させることができます。

まとめ

こうして、無事にフォームができたのでした。
かなり時間かかりましたが、なんとかなりました。
コンポーネントは複雑化させていきますので、
次はvuexを導入します。

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

vue-metaを用いたOGPやJSON-LD用のメタタグ生成

vue-meta
https://github.com/nuxt/vue-meta

・Title and Description
・OGP
・JSON-LD
を生成するにはvue-metaが便利。
弊社ではNuxtを使わず、Vue.jsで作っています。
Vue-metaはNuxtプロジェクトの中で開発しているのでVue.jsでは使えないと思っていたのですが、Vue.jsでも使えました?

この記事を書いたときのバージョン

"vue": "^2.6.10",
"vue-meta": "^2.3.1",

インストール

npm install vue-meta --save

コードの変更箇所

// src/main.ts
import VueMeta from 'vue-meta';
Vue.use(VueMeta);
// Page.vue
import {metatag} from @/metatag/metatag.ts'
export default Vue.extend({
    data() {
        return {
            this.initialized = false;
            this.param = {hoge: ‘bar'}
        }
    },
    mounted() {
        // ここでAPI通信などの非同期処理を行う。
        this.initialized = true;
    },
    metaInfo() {
      return this.initialized ? metatag(this.param) : null; // this.initializedが変更されると検知してくれる?
    },
});
// src/metatag/metatag.ts
const TITLE = ’サイト名’;

export function metatag(params: any) {
  const description = `descriptionを書くよ ${JSON.stringify(params)}`;
  return {
    title: TITLE,
    meta: [
      {name: 'description', content: description},
      {property: 'twitter:card', content: 'summary'},
      {property: 'twitter:title', content: document.title},
      {property: 'twitter:site', content: '@anonymous'},
      {property: 'twitter:creator', content: '@anonymous'},
      {property: 'twitter:description', content: description},
      {
        property: 'twitter:image',
        content: 'https://FQDN/favicon256.jpg',
      },
      {property: 'og:title', content: document.title},
      {property: 'og:description', content: description},
      {property: 'og:type', content: 'website'},
      {property: 'og:url', content: location.href},
      {
        property: 'og:image',
        content: 'https://FQDN/favicon256.jpg',
      },
    ],
    script: [
      {
        type: 'application/ld+json',
        innerHTML: JSON.stringify(
          [{
            '@context': 'http://schema.org',
            '@type': 'Organization',
            'url': location.href,
            'logo': 'https://FQDN/favicon256.jpg',
          },
            {
              '@context': 'http://schema.org',
              '@type': 'WebSite',
              'name': document.title,
              'alternateName': description,
              'url': location.href,
            },
          ], null, 2,
        ),
      },
    ],
  };
}

vue-headという類似ライブラリを使っていたんですけど、
this.$emit(‘updateHead’);
というメソッドを非同期通信完了のタイミングで実行しなければならなかったのが辛かった・・・
また、JSON-LDの書き方がすごくスッキリしました。

これで最低限のOGPとGoogle対策ができました。
とはいえ、Twitter CardやGoogleに完全対応するには、DynamicRenderingやSSRが必要です。
Vue.jsで開発しているとSSR対応は難しいので、HeaderlessChrome(puppeteer)を使ったDynamicRenderingをやることにしました。
DynamicRenderingに関しては結構長い記事なので次回更新します。

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

Vue.js + webpack で 数字をドラムロールする

はじめに

数字をドラムロールするアニメーションはすでに、Vue.jsのドキュメント にあるけど、これをwebpackで使いたい。
あと、フォームに入力してカウントアップするやり方ではなく、ボタンを押したら10ずつ増えるという簡単なコードを書きました。

HTMLを書く

ボタンと数字を表示する

<div id="app">
  <button v-on:click="countup">
    10ずつカウントアップ
  </button>

  <p>{{ animatedNumber }}</p>
</div>

GSAPをインストール

GSAPのTweenLiteを使うため、GSAPをwebpackにインストールします。

npm install gsap

コードを書きます。

import Vue from 'vue'
import TweenLite from 'gsap' // GSAPのTweenLiteを使います。

new Vue({
  el: '#app',
  data: {
    tweenedNumber: 0
  },
  computed: {
    animatedNumber: function() {
      return this.tweenedNumber.toFixed(0) //小数点を削除するため toFixedを使用
    }
  },
  methods: {
    countup: function() {
      const newValue = this.tweenedNumber + 10
      TweenLite.to(this.$data, 0.5, { tweenedNumber: newValue })
    }
  }
})

といった感じです。
これができると色々応用がきくと思います。
わたしは、検索結果の件数をリアルタイムで出す際にこれを使ってます。

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

Vue.js+TypeScript (Vue.ts) へ、Vue Test Utils+Jestを入れた際に発生したエラーを解消した軌跡

Vue.jsでTypeScriptと一緒にVue Test Utils+Jestを使う

公式の日本語ドキュメントの

に記載されている手順通り順番に導入していくと案外すんなりといかなかったので、その際に試した解決方法をここに残しておきます。
(ちなみに上から順番に解決していったのでもしかしたら余計なプラグインが入っているかもしれません

なおここで扱うテストコード(HelloWorld.spec.tsテスト対象のコード(HelloWorld.vue公式のリポジトリにあるコードと同じ想定です。

src\components\HelloWorld.vue
src\components\HelloWorld.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <p>
      For guide and recipes on how to configure / customize this project,<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank">vue-cli documentation</a>.
    </p>
    <h3>Installed CLI Plugins</h3>
    <ul>
      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank">typescript</a></li>
    </ul>
    <h3>Essential Links</h3>
    <ul>
      <li><a href="https://vuejs.org" target="_blank">Core Docs</a></li>
      <li><a href="https://forum.vuejs.org" target="_blank">Forum</a></li>
      <li><a href="https://chat.vuejs.org" target="_blank">Community Chat</a></li>
      <li><a href="https://twitter.com/vuejs" target="_blank">Twitter</a></li>
      <li><a href="https://news.vuejs.org" target="_blank">News</a></li>
    </ul>
    <h3>Ecosystem</h3>
    <ul>
      <li><a href="https://router.vuejs.org" target="_blank">vue-router</a></li>
      <li><a href="https://vuex.vuejs.org" target="_blank">vuex</a></li>
      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank">vue-devtools</a></li>
      <li><a href="https://vue-loader.vuejs.org" target="_blank">vue-loader</a></li>
      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li>
    </ul>
  </div>
</template>

<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
  name: 'HelloWorld',
  props: {
    msg: String,
  },
});
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

src\components_tests_\HelloWorld.spec.ts
src\components\__tests__\HelloWorld.spec.ts
import 'jest';
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'

describe('HelloWorld.vue', () => {
  test('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    expect(wrapper.text()).toMatch(msg)
  })
})

①、VSCode上でCannot find name 'describe'.と警告される

Visual Studio Codeでテストコードを記述した際に以下のように警告されました

Cannot find name 'describe'. Do you need to install type definitions for a test runner? Try `npm i @types/jest` or `npm i @types/mocha` and then add `jest` or `mocha` to the types field in your tsconfig.ts(2593)

①解決方法

jestをインストールし、tsconfig.jsonに以下の記述を追加

tsconfig.json
{
  "compilerOptions": {
    // ..
    "types": [
      "webpack-env",
      "jest" // typesにjestを追加
    ],
    // ..
}

②、テスト実行時にspawn jest ENOENTというエラーが発生

①を解消後、テストを実行すると以下のエラーが発生。

Error while running task C:\work\project\vue:test:unit with message 'spawn jest ENOENT'

(VueUIでのエラーログ)

FireShot Capture 007 - [Beta] test_unit - プロジェクトタスク - Vue CLI - localhost.png

以下詳細なエラー

Error: spawn jest ENOENT
    at notFoundError (C:\Users\sola\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\cross-spawn\lib\enoent.js:6:26)
    at verifyENOENT (C:\Users\sola\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\cross-spawn\lib\enoent.js:40:16)
    at ChildProcess.cp.emit (C:\Users\sola\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\cross-spawn\lib\enoent.js:27:25)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12) {
  code: 'ENOENT',
  errno: 'ENOENT',
  syscall: 'spawn jest',
  path: 'jest',
  spawnargs: []
}

②解決方法

グローバルにjestts-jestをインストール

npm i jest ts-jest -g

i ・・・installの省略記法

以下実行結果例

C:\Users\sola>npm i jest ts-jest -g
npm WARN deprecated left-pad@1.3.0: use String.prototype.padStart()

// 略

+ ts-jest@24.1.0
+ jest@24.9.0
added 476 packages from 358 contributors in 19.261s

③、テスト実行時にCannot find module 'babel-core'とエラーが発生

②を解消後、再度テストを実行。以下のように別エラーが発生

Cannot find module 'babel-core'

FireShot Capture 009 - [Beta] test_unit - プロジェクトタスク - Vue CLI - localhost.png

③解決方法

プラグインbabel-coreをインストールする

npm i babel-core

プラグイン詳細:https://www.npmjs.com/package/babel-core

※しかしながらこの際、babel-coreの最新(安定)版をインストールすると以下のエラーが発生しました。

Plugin 1 specified in "C:\\work\\project\\vue\\node_modules\\@vue\\cli-plugin-babel\\preset.js" provided an invalid property of "default" (While processing preset: "C:\\work\\project\\vue\\node_modules\\@vue\\cli-plugin-babel\\preset.js")

調べてみるとどうやらバージョン7.0.0-bridge.0を固定して入れるとエラーが解消されるみたいなので、
もし上記のエラーが発生した場合はバージョンを以下のように固定して再インストールしたほうがいいのかもしれません。

package.json
{
  "dependencies": {
    "babel-core": "7.0.0-bridge.0",
  },
}

参考:Cannot find module 'babel-core' · Issue #4891 · facebook/jest

おわり

  • フロントエンド初心者なんでなにか間違いありましたら指摘ください

参考URL

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

データの監視

算出プロパティとウォッチャ — Vue.js

watchcomputedを使ってデータを監視するときにいろいろ大変だったのでメモ。
vueではdatacomputedにあるデータを監視して、変更を検知して何かしらfunctionを起こしたりとかが出来る。

今回やりたいこと

props[quote]の変更を検知して、quote_controllerにPOSTしたい。
ただし、quoteの中にはquote.choicesquote.roomがある。

元の状態

watch: {
    quote: {
      handler: function(newValue) {
        this.$http.post('//localhost:3000/quote', {
          choices: this.choices,
          room: this.room
        }).then((res) => {
          this.quote.sections = res.data
        })
      },
      deep: true
    }
}

何が問題なの?

handlerをつけると、配列の要素そのものが変更されたことを検知する。つまり、this.room = room1 => this.room = room2とか、this.choicesの要素の一つの内容だけ変わったとかも検知してくれる。deep: trueがないと、配列の要素の増減は検知してくれるけれど内容が変わったことは検知しない。

今回、roomに関しては内容の変更も検知してもらう必要がある。でないとroomに入っているのはいつも一つの要素だけだから永遠に検知されない。
でも、choicesに関してはpatternを変更すると一気に要素が入れ替わる為、その時に要素が変わったのをいちいち検知して永遠にPOSTされてしまった。
しかし、choicesroomは同時にPOSTする必要があるので別々にwatchしてPOSTアクションを起こすことは避けたい…

解決??

これで一旦正常に動いているように見える。

watch: {
    choices: function(newVal, oldVal) {
      this.change = true
      console.log("heyyyyyy")
    },
    room: {
      handler: function(newVal, oldVal) {
        this.change = true
        console.log("wowowowowowo")
      },
       deep: true
    },
    change: {
      handler: function(newVal, oldVal) {
        if(this.change) {
          this.$http.post('//localhost:3000/quote', {
            choices: this.choices,
            room: this.room
          }).then((res) => {
            this.quote.sections = res.data
          })
          console.log("www")
          this.change = false
        }
      }
    }
}

まず。datachangeを置く。
roomだけdeep: trueの状態にして、choicesroomの変更が検知されたらchange=trueとしてchange自体の変更もwatchする。

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

Amazon S3でSPAをサクッと公開する

はじめに

ストレージサービスとして有名&優秀なAmazon S3ですが、実は「静的ウェブサイトホスティング」という機能を使うことで、Vue.jsやReactで作ったSPAを簡単に公開することができます。また、AWS CLI を使用することでコマンド一発でサクッとデプロイすることができます。herokufirebaseなどのPaaSが充実している昨今、あまりS3でやるメリットない気もしますが。今回はその手順についてまとめてみました。

(ちなみに私がVue.jsをよく使うのでちょくちょくVue.jsが登場しますが、Reactでも同様の操作ができるはずなので、適宜読み替えていただければと思います。)

お値段

まず一番大事なお金の話から。
S3の料金は「利用したストレージの容量」「リクエスト件数」「リクエストに対するデータ送信量」の3軸で計算されます。

ストレージ料金
最初の 50 TB/月 0.025USD/GB
リクエスト料金
PUT、COPY、POST、または LIST リクエスト リクエスト 1,000 件あたり 0.0047USD
GET、SELECT および他のすべてのリクエスト リクエスト 1,000 件あたり 0.00037USD
データ転送料金
1 GB まで/月 0.00USD/GB
次の 9.999 TB/月 0.114USD/GB

料金 - Amazon S3 | AWSより一部抜粋

例えば、配信するコンテンツのデータサイズが1MB、リクエスト件数が100件/日の場合、
ストレージ料金:0.000025USD
リクエスト料金:0.00111USD
データ転送料金:0.228USD
で、合計約 0.229USD/月 (日本円で約 25.19円/月)です。ばちくそ安いですね。

アカウント開設から1年以内の無料枠を利用すればほぼ0円に抑えられると思います。
※参考 - AWS クラウド無料利用枠

SPAの準備

各フレームワークでのビルドを実行し、
index.htmlと各フォルダ(css/js/img等)が揃っている状態にしましょう。
スクリーンショット 2019-11-12 9.28.31.png

ちなみにVue.jsでのプロジェクト開始方法およびビルド方法はこちらの記事(Vue CLI スタートガイド)にまとめてありますので、フロントフレームワークを全く触ったことないという方はこちらを参考にしてみてください。

S3用のIAMユーザ作成

AWSルートアカウントの作成

AWSを初めて使うという方はAWSルートアカウントを作成しましょう。
こちらを参考にすると良いと思います。
AWS アカウント作成の流れ | AWS

S3用のIAMユーザの作成

ルートアカウントは全権限アカウントのため全てのAWSリソースへのアクセスができてしまいます。このルートアカウントで作業を続けることはセキュリティ上よろしくないので、S3のみ使用可能なIAMユーザを別途作成し、今後の作業はこのS3用IAMユーザで行います。

まずルートアカウントでログインし、マネジメントコンソールからIAMへ移動します(検索バーで「iam」と打てば出てきます)。
スクリーンショット 2019-11-10 22.48.28.png

「ユーザー」メニューを選択すると、作成したIAMユーザの一覧が表示されます。
今回は新規でユーザを作成するので、青色ボタンの「ユーザーを追加」をクリックしましょう。
スクリーンショット 2019-11-10 22.52.02.png

IAMユーザを作成するための設定画面が表示されます。

  • ユーザー名は任意の名前で構いません(公開しようとしているSPA用のIAMユーザであることが分かるネーミングだと良いです)
  • 「AWS マネジメントコンソールへのアクセス」にチェックを入れ、ログインパスワードを設定しましょう
    スクリーンショット 2019-11-10 23.05.31.png

  • 「既存のポリシーを直接アタッチ」から、AmazonS3FullAccessを選択し、チェックを入れましょう
    スクリーンショット 2019-11-10 23.10.52.png

それ以外はデフォルトの設定で問題ありません。

IAMユーザの作成が完了すると一覧に表示されるので、問題なく作成されているか確認しましょう。
スクリーンショット 2019-11-10 23.19.27.png

IAMユーザの作成が完了したら、早速ログインしてみましょう。
右上にIAMユーザ名 @ アカウント名と表示されていれば問題なくログインできています。
スクリーンショット 2019-11-11 9.57.50.png

S3以外のサービスへのアクセスがブロックされているか確認するために、試しにIAMを開いてみましょう。
先ほどIAMユーザを作成した画面に移動しても「アクセス権限が必要です」と表示され、ユーザを新規作成できないようになっているはずです。
スクリーンショット 2019-11-11 10.01.22.png

このように、利用用途ごとにIAMユーザを作成してAWSサービスへの権限を切り分け、意図しない操作が実行されないようにしましょう。

S3でSPAを公開

先ほどログインしたS3用IAMユーザで作業を進めます。

バケットの作成

S3の画面へ移動し、青色の「+バケットを作成する」ボタンをクリックしましょう。
バケット作成に際しての設定画面が表示されるので、情報を入力しましょう。

  • バケット名:任意の文字で構いません。ここで設定したバケット名が後ほど静的ホスティングする際のURLの一部として使われるので、それっぽい名前を付けましょう。
  • リージョン:これも任意で構いませんが、「アジアパシフィック(東京)」を選択するのが無難です。 スクリーンショット 2019-11-11 10.23.26.png

それ以外の設定は一旦デフォルトのままで問題ないです。

バケットが作成されるとバケット一覧に表示されるようになります。
スクリーンショット 2019-11-11 10.31.27.png

コンテンツのアップロード

バケット名をクリックするとバケットの詳細画面に遷移することができます。
青色の「アップロード」ボタンをクリックし、公開したいSPAの各ファイル(index.htmlとcss/js/imgフォルダ等)をローカルからアップロードしましょう。
初回アップロード時にバケットの設定について色々と聞かれますが、全てデフォルトで問題ないです。
無事アップロードが完了するとこのような画面になると思います。
スクリーンショット 2019-11-11 10.49.53.png

静的ウェブサイトホスティング機能の設定

「プロパティ」タブへ移動し、「Static website hosting」の設定を行います。

  • 「このバケットを使用してウェブサイトをホストする」にチェックを入れます
  • 「インデックスドキュメント」にindex.htmlを指定します スクリーンショット 2019-11-11 10.52.54.png

上記の設定が完了したら「保存」ボタンをクリックしましょう。

なお、この画面で表示されている「エンドポイント」のURLがウェブサイトへのアクセス用URLになります。が、今の状態でこのURLにアクセスしようとしても403エラーが返ってきてしまいます。URLでのアクセスを許可するために、次の章で説明するバケットポリシーを設定しましょう。
スクリーンショット 2019-11-11 11.00.11.png

バケットポリシーの設定

S3に格納したオブジェクトが不用意にネットに晒されないよう、デフォルトでは外部からS3バケットへのアクセスは全て拒否するようになっています。先ほど403エラーが返ってきたのもそのためです。正しくWebページを表示させるためにはURLによる外部からのリクエストを明示的に許可する必要があります。

アクセス制御に関することは「アクセス制御」タブで行います。

まず、「ブロックパブリックアクセス」を開きます。「編集」をクリックし、以下の設定を行います。

  • 「パブリックアクセスをすべてブロック」のチェックを外します
  • 下から2つ目、「新しいパブリックバケットポリシーを介して...」のチェックを外します
  • 一番下、「任意のパブリックバケットポリシーを介して...」のチェックを外します スクリーンショット 2019-11-11 22.28.31.png

これでバケットポリシーによるアクセス許可設定が有効になります。ここの設定を行わないと、いくらバケットポリシーで許可の設定を行ってもブロックされてしまうので注意しましょう。

次に「バケットポリシー」を開き、エディタ欄に以下のJSONをバケット名を置き換えて貼り付けましょう。
このJSONはsample-hosting-kiyokiyoバケットへのGetリクエストを許可するバケットポリシーです。AWS公式チュートリアルのものをそのまま抜粋しました。バケット名の部分のみ、自分が作成したバケット名に置き換えるのを忘れないようにしましょう。

{
   "Version":"2012-10-17",
   "Statement":[{
    "Sid":"PublicReadForGetBucketObjects",
         "Effect":"Allow",
      "Principal": "*",
       "Action":["s3:GetObject"],
       "Resource":["arn:aws:s3:::sample-hosting-kiyokiyo/*"
       ]
     }
   ]
 }

これで外部からS3に格納したファイルを取得できるようになりました。
先ほど403エラーが返ってきたURLでアクセスし直すと、今度は正しくWebページが表示されるようになっているはずです。
スクリーンショット 2019-11-11 22.45.49.png

S3へのデプロイコマンドを作る

上記の手順でWebページの公開はできるようになりましたが、Webページを更新するたびにIAMユーザでログインし、S3バケットに格納してあるファイルを削除して、ローカルにある新しいファイルをアップロードし直す、というのはかなり面倒です。Vue.jsでは
npm run serveでローカルサーバーを起動し、
npm run buildでビルドを行うことができます。
それと同じノリで、
npm run deployで、S3へのデプロイができるよう設定を組みましょう。

AWS CLI を使用する

AWS CLI を使うと、ターミナル等のコマンドラインツールからAWSサービスを操作できるようになります。これを利用して、S3上の対象バケットにあるファイルを削除し、ローカルにある新規ファイルをアップロードするスクリプトを組みます。

こちらの記事でAWS CLI を使うための手順がまとめられているので参考にしてみてください。
【初心者向け】MacユーザがAWS CLIを最速で試す方法 | Developers.IO

かいつまんで説明しますと、

1:まずpipをインストールします(Python3.4以降であればPythonのインストールと同時に使えるそうです)。pip -Vと打ってバージョンが表示されれば問題ないです。

$ pip -V
pip 19.3.1 from /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/pip (python 3.7)

2:次にAWS CLI をインストールします

$ pip install awscli

3: ルートアカウントでS3用IAMユーザのアクセスキーとシークレットアクセスキーを生成・取得します。IAMメニューで先ほど作成したS3用IAMユーザを選択し、「認証情報」タブの「アクセスキーの作成」ボタンをクリックします。アクセスキーとシークレットアクセスキーが表示されるので、手元に控えておきましょう。
スクリーンショット 2019-11-11 23.24.42.png

4:ターミナルにaws configureと入力しS3を操作するための設定を行います。Access Key IDSecret Access Keyに先ほど取得した情報を入力しましょう。Default regionはS3バケットで指定したリージョン(アジアパシフィック(東京)の場合はap-northeast-1)を入力しましょう。Default output formatはとりあえずtextで問題ありません。

$ aws configure
AWS Access Key ID [None]: XXXX
AWS Secret Access Key [None]: XXXXXXXX
Default region name [None]: ap-northeast-1
Default output format [None]: text

動作確認として、aws s3 lsでS3に登録しているバケットが一覧表示されればOKです。

$ aws s3 ls
2019-11-11 22:45:22 sample-hosting-kiyokiyo

デプロイ用のシェルスクリプトを組む

index.html等が格納されているディレクトリをdistとします。
distと同じ階層にデプロイ用のスクリプトを記載したdeploy-s3.shを配置します。
ディレクトリ構造のイメージはこんな感じです。

(any directory)
 ├dist/
 │ ├css/
 │ ├img/
 │ ├js/
 │ └index.html
 └deploy-s3.sh

deploy-s3.shの中身はこんな感じで書きます。

deploy-s3.sh
#!/bin/sh

aws s3 rm s3://sample-hosting-kiyokiyo/ --recursive
aws s3 cp dist s3://sample-hosting-kiyokiyo/ --recursive

1行目はシェルスクリプトを走らせるためのおまじないです。詳しく知りたい方はこちらの記事(#!/bin/sh は ただのコメントじゃないよ! Shebangだよ!)とかが参考になると思います。
2行目ではsample-hosting-kiyokiyoバケットの中身を再帰的(--recursive)に削除(rm)しています。
3行目ではdistディレクトリの中身をsample-hosting-kiyokiyoバケットにコピー(cp)しています。

AWS CLI でできることはこちらのAWS CLI Command Referenceにまとまっているので、他のスクリプトを走らせたい方は調べてみてください。

デプロイ用コマンドを作る

最後に、npm run deployと入力したら先ほど作成したdeploy-s3.shが呼び出されるようにします。
npm runコマンドはpackage.jsonscriptsブロックで設定できます。
"deploy": "bash deploy-s3.sh"をscriptsブロック内に追加しましょう。

package.json
{
  (省略)
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "deploy": "bash deploy-s3.sh"
  },
  (省略)
}

これで作業としては完了です。
試しにローカルでの変更をS3にデプロイできるか確かめてみましょう。

まず、ローカルで適当に変更を行います。
サンプルとして今回はとりあえずApp.vueのHelloWorldタグを以下のように変えてみます。

(変更前)
<HelloWorld msg="Welcome to Your Vue.js App"/>
(変更後)
<HelloWorld msg="Hi! My name is Kiyokiyo! Nice to meet you!"/>

ビルドコマンドを走らせます。

$ npm run build

これでローカルのdistディレクトリ以下に必要なファイルが揃いました。

最後にデプロイコマンドを走らせます。

$ npm run deploy

S3上のファイルがdeleteされ、ローカルのファイルがS3にアップロードされたことが、ターミナルの出力からも分かると思います。

WebページのURLにアクセスするとしっかり変更が反映されていますね。
スクリーンショット 2019-11-12 9.17.36.png

おわりに

これでローカルで作成していた静的ウェブサイトを公開できるようになりました。
デプロイもコマンド一発で簡単にできるようになったので、開発速度もかなり向上したんじゃないでしょうか。

個人的な今後としては、LambdaやDynamoDBを利用したサーバーレスAPIとの通信にチャレンジしてみたいと思います。

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

Vue.js + axios でヘッダーにログイン情報を毎回セットする

axios の create() と interceptors() を使ってラップした http plugin を作ると良いかもしれません。

src/plugins/http.ts
import _Vue from 'vue';
import axios from 'axios';

export default {
  install(Vue: typeof _Vue): void {
    const http = axios.create({
      // URL は環境変数とかで変えられるにする
      baseURL: 'http://localhost:3000/',
      timeout: 10000,
    });
    http.interceptors.request.use((config: any) => {
      // $stores.auth.show に認証情報が入っているとする
      config.headers = Vue.prototype.$stores.auth.show;
      return config;
    });
    Vue.prototype.$http = http;
  },
};

main.ts のどこかに下記を書きます。
すると、plugin が使えるようになります。

src/main.ts
// 略

import http from '@/plugins/http';
Vue.use(http);

認証情報の store はこんなイメージです。
vuex-module-decorators を使っています。

src/stores/AuthStore.ts
import Vue from 'vue';
import { Module, Mutation, VuexModule } from 'vuex-module-decorators';
import LoginInterface from '@/interfaces/LoginInterface';
import { AuthStoreInterface } from '@/interfaces/StoresInterface';
import { AuthResponseInterface, ErrorResponseInterface } from '@/interfaces/ResponseInterface';

@Module({ name: 'auth' })
export default class AuthStore extends VuexModule implements AuthStoreInterface {
  public token = '';
  public user_id  = 0;

  public get show(): object {
    return {
      Authorization: `Token ${this.token}`,
      'User-Id': this.user_id,
    };
  }

  @Action
  public create(users: LoginInterface): void {
    Vue.prototype.$http.post('/users/auth', { user: users })
    .then((res: AuthResponseInterface): void => {
      this.set_token(res.data.token);
      this.set_user_id(res.data.id);
    })
    .catch((err: ErrorResponseInterface): void => {
      // エラー処理
    });
  }

  @Mutation
  private set_token(token: string): void {
   this.token = token;
  }

  @Mutation
  private set_user_id(user_id: number): void {
    this.user_id = user_id;
  }
}

そうすると Store 内でVue.prototype.$http.get()Vue.prototype.$http.post()すると AuthStore のログイン情報を毎回見に行ってくれます。
vue ファイルから呼び出す際は this.$http.get()this.$http.post()でリクエストできます。

もし vuex-persist を使っていれば、リロードしても消えません。

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

Vue.js + axios でヘッダーにログイン情報を毎回セットする。

plugin を自作して axios の create() と interceptors() を使ってラップした http plugin を作ると良いかもしれません。

src/plugins/http.ts
import _Vue from 'vue';
import axios from 'axios';

export default {
  install(Vue: typeof _Vue): void {
    const http = axios.create({
      // URL は環境変数とかで変えられるにする
      baseURL: 'http://localhost:3000/',
      timeout: 10000,
    });
    http.interceptors.request.use((config: any) => {
      // $stores.auth.show に認証情報が入っているとする
      config.headers = Vue.prototype.$stores.auth.show;
      return config;
    });
    Vue.prototype.$http = http;
  },
};

Typescript で書いているため、上記 plugin の型ファイルを *.d.ts という拡張子にしてどこかに置きます。

src/types/http.d.ts
import Vue from 'vue';
import { AxiosStatic } from 'axios';

declare module 'vue/types/vue' {
  interface Vue {
    $http: AxiosStatic;
  }
}

あとは main.ts のどこかに下記を書きます。
すると、自作 plugin が使えるようになります。

src/main.ts
// 略

import http from '@/plugins/http';
Vue.use(http);

認証情報の store はこんなイメージです。
vuex-module-decorators を使っています。

src/stores/AuthStore.ts
import Vue from 'vue';
import { Module, Mutation, VuexModule } from 'vuex-module-decorators';
import LoginInterface from '@/interfaces/LoginInterface';
import { AuthStoreInterface } from '@/interfaces/StoresInterface';
import { AuthResponseInterface, ErrorResponseInterface } from '@/interfaces/ResponseInterface';

@Module({ name: 'auth' })
export default class AuthStore extends VuexModule implements AuthStoreInterface {
  public token = '';
  public user_id  = 0;

  public get show(): object {
    return {
      Authorization: `Token ${this.token}`,
      'User-Id': this.user_id,
    };
  }

  @Action
  public create(users: LoginInterface): void {
    Vue.prototype.$http.post('/users/auth', { user: users })
    .then((res: AuthResponseInterface): void => {
      this.set_token(res.data.token);
      this.set_user_id(res.data.id);
    })
    .catch((err: ErrorResponseInterface): void => {
      // エラー処理
    });
  }

  @Mutation
  private set_token(token: string): void {
   this.token = token;
  }

  @Mutation
  private set_user_id(user_id: number): void {
    this.user_id = user_id;
  }
}

そうすると Store 内でVue.prototype.$http.get()Vue.prototype.$http.post()すると AuthStore のログイン情報を毎回見に行ってくれます。
vue ファイルから呼び出す際は this.$http.get()this.$http.post()でリクエストできます。

もし vuex-persist を使っていれば、リロードしても消えません。

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

VeturにTypeScript3.7のOptional Chainingを適用してみる

TL;DR

settings.json
{
    // Use dependencies from workspace. Currently only for TypeScript.
    "vetur.useWorkspaceDependencies": true, 
}

概要

TypeScript 3.7のOptional Chainingは最高!!
いつも「なんで?.で書けないんだ」って思っていたけど、ついに来た!

ただ...VueというかVeturで使うには...
Veturは固定のTypeScriptバージョンに依存している1
最新サポートしているバージョンはVetur0.22.3のTypeScript3.6.3。
このままTypeScript 3.7のOptional Chainingを書いたら、Veturに怒られる
コメント 2019-11-11 232438.png
トランスパイルしているのはあくまでtscなので、実害があるわけじゃないが、Intelligenceが効かなくなるのは相当な痛手だ。

解決策

探してみたらちゃんとあった、

Allow using workspace typescript version#682
how can i use optionalChaining?#1438

Vetur 0.17.0以降、"vetur.useWorkspaceDependencies"のオプションを設定できるらしい。
字面通りの意味なら、この設定をしたら、ワークスペースのTypeScriptバージョンを使ってくれるみたい!

ワークスペースのTypeScriptのバージョンを3.7.2に上げて、

$ npm install typescript@3.7.2 --save-dev

ESLintのparserの@typescript-eslint/parserを最新バージョンの2.7.0に更新する。(3.7.2対応のparserにしないと表示が色々おかしくなる)

$ npm install @typescript-eslint/parser@2.7.0 --save-dev

VSCodeのsettings.json

settings.json
{
    // Use dependencies from workspace. Currently only for TypeScript.
    "vetur.useWorkspaceDependencies": true, 
}

を追加したら、Optional Chainをちゃんと認識してくれるようになった!(VSCodeの再起動必須)
スクリーンショット (2).png

使用環境

TypeScript 3.7.2
VSCode 1.40.0
Vetur 0.22.6
ESLint 5.16.0

ESLint 設定

.eslintrc.js
module.exports = {
    parserOptions: {
        parser: '@typescript-eslint/parser',
    },

    extends: [
        // ...
    ],
};

  1. how can i use optionalChaining?#1438 Comment https://github.com/vuejs/vetur/issues/1438#issuecomment-533952010 

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