20190411のvue.jsに関する記事は7件です。

DjangoのテンプレートにVue.jsを導入してaxiosでAjaxしてみた話(後編)

はじめに

前編では,Ajax通信でサーバ側(のDjango)からクライアント側(のVue.js)にデータをもってくることができた.後編では,逆にクライアント側(のVue.js)からサーバ側(のDjango)にデータを送って必要に応じてデータベースを更新することを試みる.

全体のコードはまとめてGitHubに置いた.

DjangoのFormによる新規タスクの追加

最初に,Ajaxを使わずに,クライント側からDjangoのFormでデータを取得する方法を振り返っておこう.そのために,新規タスクを追加することを考える.新規タスクはすべて未完了だと仮定して,次のようなFormを用意した(新たにFormを定義せず,ModelFormを利用する手もあるが).

from django import forms
import datetime
from . import models

class TodoForm(forms.Form):
    task = forms.CharField(max_length=255)
    due = forms.DateTimeField(
        widget=forms.DateTimeInput(format="%Y-%m-%d"),
        initial = datetime.date.today()
        )

    def save(self):
        new_task = self.cleaned_data["task"]
        its_due = self.cleaned_data["due"]
        if models.Todo.objects.filter(task=new_task).count() == 0:
            todo = models.Todo(task=new_task, due=its_due, done=False);
            todo.save()

続いて,このFormを扱うview関数

def add_todo(request):
    if request.method == "POST":
        form = forms.TodoForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect("which_url_to_be_redirected_to")
    else:
        form = forms.TodoForm()
    todos = models.Todo.objects.all().order_by("due")
    my_context = {
        "form":form,
        "todos":todos,
        }
    return render(request, "name_of_your_template", my_context)

とFormを埋め込むテンプレート

<html>
  <head><title>Todo</title></head>

  <body>
    <h1>My Todo List</h1>
    <div>
      <ul>
        {% for item in todos %}
          <li>
            {% if item.done %}
              <span style="text-decoration: line-through; color: red">
                {{ item.task }}
                ({{ item.due|date:"n" }}/{{ item.due|date:"j" }})
              </span>
            {% else %}
              <span>
                {{ item.task }}
                ({{ item.due|date:"n" }}/{{ item.due|date:"j" }})
              </span>
            {% endif %}
          </li>
        {% endfor %}
      </ul>
      <hr>
      <form method="post" action="{% url 'of_this_page_itself' %}">{% csrf_token %}
        {{ form }}
        <button type="submit" >Add</button>
      </form>
    </div>
  </body>
</html>

を作成した.これで,既存タクスのリストの下に新規タスクを追加するための入力フォームと登録ボタンが表示されるようになった.Formを利用したデータの取得は定石的な流れなので,比較的簡単に実装できるようになっている.

ポイントは,フォーム要素(<form></form>)の中に埋め込んだ{% csrf_token %}である.これを埋め込んでおくだけでDjango側でcsrfの対策が講じられる.逆に,これを埋め込むのを忘れると,DjangoにPOST通信を受け付けてもらえない.

Vue.jsでの新規タスクの追加

次に,Vue.jsで新規タスクを追加できるようにしてみよう.そのために,前編の最後のテンプレートを下記のように少し拡張する.

<html>
  <head><title>Todo</title></head>
  <body>
    <h1>My Todo List</h1>
    <div id="app">
      <ul v-cloak v-for="item in todos">
        <li v-on:click="changeStatus(item)">
          <span style="text-decoration: line-through; color: red" v-if="item.done">
            [[ item.task ]]
            ([[ item.due.getMonth()+1 ]]/[[ item.due.getDate() ]])
          </span>
          <span v-else>
            [[ item.task ]]
            ([[ item.due.getMonth()+1 ]]/[[ item.due.getDate() ]])
          </span>
        </li>
      </ul>
      <hr>
      <form v-on:submit.prevent="addTask">
        <label for="task">Task:</label>
        <input type="text" id="task" v-model="new_task">
        <label for="due">Due:</label>
        <input type="date" id="due" v-model="its_due">
        <button type="submit" >Add</button>
      </form>
    </div>

    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script>

var vm = new Vue({
  delimiters: ['[[', ']]'],
  el: '#app',
  data: {
    todos: [],
    new_task: "",
    its_due: ""
  },
  mounted: function() {
    axios.get('{% url "for_view_function_get_todo()" %}')
      .then(function (response) {
        for(var d in response.data) {
          var item = response.data[d];
          item.due = new Date(item.due);
          vm.todos.push(item);
        }
      })
      .catch(function (error) {
        console.log(error);
      })
      .then(function () {
      });
    },
  methods: {
    changeStatus: function(item) {
      item.done = !item.done;
    },
    addTask: function() {
      if(this.new_task && this.its_due){
        this.todos.push({
          "task":this.new_task,
          "due": new Date(this.its_due),
          "done":false
        });
      }
      this.new_task = "";
      this.its_due = "";
    }
  }
})

    </script>
  </body>
</html>

上部のhtmlの部分を見てみると,DjangoのFormを利用した場合と同じ箇所にフォーム要素(<form></form>)が挿入され,サブミットイベントを処理するディレクティブが追加されていることがわかる.この際に呼び出されるのがaddTaskメソッドである.

このaddTaskメソッドの本体は,下部のVue.jsのスクリプトの中に追加されている.具体的には,new_taskits_dueに情報が入っていることを確認し,それらの情報をもとに新しいtodoのオブジェクトを生成した上で,それをtodosに追加し,最後にnew_taskits_dueを空に戻している.

なお,new_taskits_dueはVueインスタンスのdataの中に追加されており,それらはv-modelディレクティブでフォームに入力されたデータと紐付けてある.これで,Vue.jsの方でも(ページ上で)新規タスクを追加できるようになった.

Ajax通信(その2,POST編)

ただし,まだデータベースは更新していないので,このままではページをリロードすると新しく追加したタスクは消えてしまう.そこで,前編とは逆に,Vue.js側からDjango側にAjax通信でJSON形式の情報を送り込んで,データベースを更新することを試みよう.ここからが後編の本題である.

このAjaxリクエストを処理するview関数から始めよう.これは次のように構成した.

def post_todo(request):
    if request.method == 'POST' and request.body:
        json_dict = json.loads(request.body)
        task = json_dict['task']
        due = json_dict['due']
        done = json_dict['done']
        todos = models.Todo.objects.filter(task=task)
        if not todos:
            models.Todo.objects.create(task=task, due=due, done=done);
        else:
            todos[0].due=due
            todos[0].done=done
            todos[0].save()
        return JsonResponse(json_dict)
    else:
        return HttpResponseServerError()

POST通信のリクエストから必要なJSONデータを抽出し,pythonの辞書に変換するには,request.bodyjson.loads()に渡せばいいらしい.

そして,得られた辞書からタスク(task),納期(due),完了済みかどうか(done)の情報を取り出し,同じ名称のタスクがなければ,それを新しいタスクとして生成しデータベースに格納している.すでに同じ名称のタスクがあった場合は,そのタスクの情報を上書きしている.

一方,Vue.js側のメソッドは,GET通信の場合と同様に考えると,例えば次のようになる.

updateDB: function(item) {
  data = {
      "task":item.task,
      "due": new Date(item.due),
      "done":item.done
    };
  axios.post('{% url "for_view_function_post_todo()" %}')
    .then(function (response) {
      console.log(response);
    })
    .catch(function (error) {
      console.log(error);
    });
}

しかし残念ながら,このままではDjangoはこのリクエストを受け付けてくれない.これは,後編の最初に書いたcsrf対策のためである.Ajax通信の場合は{% csrf_token %}を埋め込むことはできないので,これに変わる方法が必要となる.

詳細はDjangoの公式ドキュメントのここを参照してほしいが,要するに,Cookieから必要な情報(csrfトークン)を抽出してそれをX-CSRFTokenという名称でPOST通信のリクエストのヘッダに書き込めばいいということらしい.

公式ドキュメントにはcsrfトークンを抽出するためのjQueryのコードも紹介されているが,ここでもそのためだけにjQueryをロードするのは避けたいので,もうひとつの手段として紹介されているJavaScriptクッキーライブラリを利用することにする.これも次のタグでCDNからロードしておこう.

<script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>

これを用いて,上のupdateDBメソッドを次のように拡張すると無事動くようになった.

updateDB: function(item) {
  csrftoken = Cookies.get('csrftoken');
  headers = {'X-CSRFToken': csrftoken};
  data = {
      "task":item.task,
      "due": new Date(item.due),
      "done":item.done
    };
  axios.post('{% url "todo:post_todo" %}', data, {headers: headers})
    .then(function (response) {
      console.log(response);
    })
    .catch(function (error) {
      console.log(error);
    });
}

他の部分も含めたテンプレートの全体は次の通りである.

<html>
  <head><title>Todo</title></head>

  <body>
    <h1>My Todo List</h1>
    <div id="app">
      <ul v-cloak v-for="item in todos">
        <li v-on:click="changeStatus(item)">
          <span style="text-decoration: line-through; color: red" v-if="item.done">
            [[ item.task ]]
            ([[ item.due.getMonth()+1 ]]/[[ item.due.getDate() ]])
          </span>
          <span v-else>
            [[ item.task ]]
            ([[ item.due.getMonth()+1 ]]/[[ item.due.getDate() ]])
          </span>
        </li>
      </ul>
      <hr>
      <form v-on:submit.prevent="addTask">
        <label for="task">Task:</label>
        <input type="text" id="task" v-model="new_task">
        <label for="due">Due:</label>
        <input type="date" id="due" v-model="its_due">
        <button type="submit" >Add</button>
      </form>
    </div>

    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
    <script>

var vm = new Vue({
  delimiters: ['[[', ']]'],
  el: '#app',
  data: {
    todos: [],
    new_task: "",
    its_due: ""
  },
  mounted: function() {
    axios.get('{% url "todo:get_todo" %}')
      .then(function (response) {
        for(var d in response.data) {
          var item = response.data[d];
          item.due = new Date(item.due);
          vm.todos.push(item);
        }
      })
      .catch(function (error) {
        console.log(error);
      })
      .then(function () {
      });
    },
  methods: {
    changeStatus: function(item) {
      item.done = !item.done;
      this.updateDB(item);
    },
    addTask: function() {
      if(this.new_task && this.its_due){
        item = {
          "task":this.new_task,
          "due": new Date(this.its_due),
          "done":false
        };
        this.todos.push(item);
        this.updateDB(item);
      }
      this.task = "";
      this.due = "";
    },
    updateDB: function(item) {
      csrftoken = Cookies.get('csrftoken');
      headers = {'X-CSRFToken': csrftoken};
      data = {
          "task":item.task,
          "due": new Date(item.due),
          "done":item.done
        };
      axios.post('{% url "todo:post_todo" %}', data, {headers: headers})
        .then(function (response) {
          console.log(response);
        })
        .catch(function (error) {
          console.log(error);
        });
    }
  }
})

    </script>
  </body>
</html>

よく見るとツッコミどころはたくさんありそうだけど,ひとまず目標は達成できたのでよしとする.

まとめ

前編,後編を通して,DjangoのテンプレートにVue.jsを導入してちょこっと使ってみるための基礎はできたと思う.Vue.js自体の機能はまだほんの触りしか使っていないので,これからいろいろ試してみたい.

ここまで読んでくださった方に感謝. m(__)m

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

DjangoのテンプレートにVue.jsを導入してaxiosでAjaxしてみた話(前編)

はじめに

Vue.jsが便利そうなのでDjangoと組み合わせて使ってみようと試してみた.使ってみたいといっても,大それたことしたいわけではなく,まずはDjangoのテンプレートの中にVue.jsのスクリプトを埋め込んでクライアント側でちょこっとした機能を実現できるようになるあたりまでが目標だ.

架空のTodoアプリを具体例として話を進めていくが,これはあくまで例示のためで,アプリ自体は実用には程遠いので悪しからず...

全体のコードはまとめてGitHubに置いた.

Django側での雛形の作成

最初に,Todoアプリに登録したいタスクのモデルを設計しておく.具体的には,次のように,タスクの名称(task),納期(due),完了済みかどうか(done)の3つのフィールドをもつデータモデル(Todo)を定義した.

class Todo(models.Model):
    task = models.CharField(max_length=255, unique=True)
    due = models.DateTimeField()
    done = models.BooleanField()

    def __str__(self):
        return self.task

後で利用するために,このデータモデルをadminサイトに登録し,adminサイトから架空のタスクをいくつか追加しておこう.

続いて,データベース内のタスクをすべて取得して納期順に並べて表示するページの雛形をつくる.具体的には,次のようなview関数

def show_todo(request):
    todos = models.Todo.objects.all().order_by("due")
    my_context = {
        "todos":todos,
        }
    return render(request, "name_of_your_template", my_context)

とテンプレート

<html>
  <head><title>Todo</title></head>
  <body>
    <h1>My Todo List</h1>
    <div>
      <ul>
        {% for item in todos %}
          <li>
            {% if item.done %}
              <span style="text-decoration: line-through; color: red">
                {{ item.task }}
                ({{ item.due|date:"n" }}/{{ item.due|date:"j" }})
              </span>
            {% else %}
              <span>
                {{ item.task }}
                ({{ item.due|date:"n" }}/{{ item.due|date:"j" }})
              </span>
            {% endif %}
          </li>
        {% endfor %}
      </ul>
    </div>
  </body>
</html>

を用意した.urlルーティングを適切に設定してページが表示されれば準備OKだ.

テンプレートへのVue.jsの導入

土台となる雛形ができたので,このテンプレートにVue.jsを導入していこう.簡単に試してみるには,次のように,CDNからロードするのが手っ取り早い.

<script src="https://unpkg.com/vue"></script>

今回はお試しなので,Vue.jsの具体的なスクリプトもこのテンプレートの中に直接書き込んでいくことにする.Vueのインスタンスを作成し,タスクの情報をリアクティブにするためにdata内のプロパティに取り込み,簡単な機能をまずひとつ実装してみた.

<html>
  <head><title>Todo</title></head>
  <body>
    <h1>My Todo List</h1>
    <div id="app">
      <ul v-cloak v-for="item in todos">
        <li v-on:click="changeStatus(item)">
          <span style="text-decoration: line-through; color: red" v-if="item.done">
            [[ item.task ]]
            ([[ item.due.getMonth()+1 ]]/[[ item.due.getDate() ]])
          </span>
          <span v-else>
            [[ item.task ]]
            ([[ item.due.getMonth()+1 ]]/[[ item.due.getDate() ]])
          </span>
        </li>
      </ul>
    </div>

    <script src="https://unpkg.com/vue"></script>
    <script>

var vm = new Vue({
  delimiters: ['[[', ']]'],
  el: '#app',
  data: {
    todos: []
  },
  mounted: function() {
    {% for item in todos %}
    this.todos.push({
      "task": "{{ item.task }}",
      "due": new Date({{ item.due|date:'U' }} * 1000),
      "done": {{ item.done|lower }},
    })
    {% endfor %}
  },
  methods: {
    changeStatus: function(item) {
      item.done = !item.done;
    }
  }
})

    </script>
  </body>
</html>

DjangoのテンプレートにVue.jsを導入する際には{{}}の扱いに注意する必要がある.デフォルトのままだと,双方でこの表記を使うことになるのでぶつかってしまう.これを回避するために個人的に気にいっている方法は,Vue.js側で{{}}を,例えば,[[]]に変更してしまうことだ.

これは,上の例にもあるように,Vueインスタンスを生成する際に次のように指定すればよい.

delimiters: ['[[', ']]']

次に,mounted:の項目を見てほしい.まだAjax通信を実装していないので,苦肉の策として,Djangoからcontextとして渡されたtodosからタスクの情報をforループで取り出し,JavaScriptのオブジェクトに変換してからVue.js側のtodosに順に追加していっている.

ひとまずこれで動いた.この部分は,後でAjax通信に変更する.なお,考えてみれば当然だが,Vue.jsのスクリプトの中でDjangoのテンプレートタグが使えていることが確認できる.

html側のリスト表示(<ul></ul>)が,Djangoのcontextではなく,Vue.jsのdataからタスクの情報を引き出す形に変更されていることにも注意する.また,リスト要素(<li></li>)にクリックイベントを処理するディレクティブも追加してある.

これによって,リスト要素をクリックすると,methods:の項目で定義されているchangeStatusが呼び出され,対応するタスクのdoneのブール値が裏返る.

Ajax通信(その1,GET編)

続いて,Ajax通信を実装していこう.Vue.js自体にはAjax通信の機能は用意されていないらしいので,まずどういう策を取るかを決めなければならない.Vue.jsを導入していけはjQueryを使う機会は減っていくだろうから,Ajax通信のためだけにjQueryをロードするのは気が引ける.そういう場合にはaxiosがおすすめらしい(ので.それに従うことにする).

まず,Vue.jsの場合と同じように,axiosもCDNからロードしておく.

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

その上で,テンプレート内のVue.jsのスクリプトを下記のように更新する.

var vm = new Vue({
  delimiters: ['[[', ']]'],
  el: '#app',
  data: {
    todos: []
  },
  mounted: function() {
    axios.get('{% url "for_view_function_get_todo()" %}')
      .then(function (response) {
        for(var d in response.data) {
          var item = response.data[d];
          item.due = new Date(item.due);
          vm.todos.push(item);
        }
      })
      .catch(function (error) {
        console.log(error);
      })
      .then(function () {
      });
  },
  methods: {
    changeStatus: function(item) {
      item.done = !item.done;
    }
  }
})

mounted:の項目の中身がaxiosを利用したAjaxのGET通信に置き換わっている.中身を見ると,JSON形式で取得したタスクのリストをresponse.dataからforループで取り出し,順にtodosに追加していっていることがわかる.また,この際に,納期(due)は,JavaScriptのDateオブジェクトに変換している.

テンプレートの側はこれでOKだ.ただし,このAjaxリクエストに対して,Django側がレスポンスを返してくれないと話にならない.続いて,Django側の設定を行おう.

まずこのリクエストを処理するために,下記のようなview関数を用意する,

import json
from django.http import JsonResponse
from . import models

def get_todo(request):
    todos = models.Todo.objects.all().order_by("due").values()
    todolist = list(todos)
    return JsonResponse(todolist, safe=False)

そして,上のaxios.get()で,このview関数が呼ばれるようにurlルーティングを設定する.

実は,ここで詰まってかなりの時間を浪費した.ポイントは,クエリセットの末尾に.values()を追加すること,それをlist()でリストに変換してから返すこと,そしてJsonResponse()safe=Falseを指定することである(このページに助けられた,感謝).

なお,テンプレートの中ではもうDjango側のtodosは使わなくなったので,ページ全体の方のview関数でそれを作成してcontextに含める処理は不要になる.したがって,こちらのview関数は,例えば次のように簡略化してしまえる.

from django.views import generic

class ShowTodoView(generic.TemplateView):
    template_name = "todo/show_todo.html"

まとめ

前編では,Vue.jsをDjangoのテンプレートに導入し,Django側からAjaxでとってきたJSON形式の情報をVue.js側で取得して処理するところまでできた.話は後編に続く.後編では,逆に,Vue.js側の情報をDjango側に送ってデータベースを更新することを試す.

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

インスタンスの外からメソッドを呼ぶ

 初投稿です。インスタンスの外からメソッドを呼びたいときがあります。

Vue インスタンスはマウントした要素の __vue__ プロパティにセットされるそうなので、例えば次のようなコンポーネントがあったとき

<template>
  <div id="awesome-element" />
</template>

<script>
export default {
  methods: {
    awesomeMethod () {
      // ...
    },
  },
}
</script>

次のようにメソッドを呼ぶことができます。

document.querySelector('#awesome-element').__vue__.awesomeMethod()

これがいつ必要になるんだという話ですが、WKWebView とか WebView とかそういう事情があります。

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

JWT認証をVueとFlaskで実装する

注意

つい最近Vueに触った素人なので色々間違ってる可能性があります。
なんとなくですがVue側のソースはキャメルケース、Pythonはスネークケースで書いてます。
個人的にはスネークケースが好きなんですけどね。

概要

フロントエンドをVue CLI 3、バックエンドをFlask(Python)で実装します。
FlaskでJWTを使うにはFlask-JWTというライブラリがありますが、ファイルの分割で問題が起きたのでExtendedを使います。
(具体的には忘れた)
こちらは情報が少ないですがかなり使いやすいと思ってます。

環境

macOS Mojave 10.14.3
vue-cli 3.2.3
Python 3.6.5
MySQL 15.1

これらの導入については解説しません。

ディレクトリ構造(完成後)
login_sample/
├── api/
└── ui/

フロントエンド

とりあえず準備と簡単なログインページまで作ります。

プロジェクトの作成

まず適当な場所にプロジェクト名のフォルダを作成しておきます。

$ mkdir login_sample

以下コマンドで、ブラウザからVue CLIをGUIで操作できるようになります。

$ vue ui

作成タブの下のボタンを押しプロジェクトを作成します。
名前を「ui」にし、プロジェクトの作成場所を「login_sample」に変更してください。
また、設定は以下の写真の通りに設定してください。(よくわかってないです)
RouterはSPA(シングルページアプリケーション)にするためです。
スクリーンショット 2019-04-12 1.10.14.png
ヒストリーモードはオフのままにしておき、linterは適当に「Standard config」を選んで作成します。
プリセットを保存するか聞かれるので念のため保存しておきます。

以下の画面で「タスクを実行」→「アプリを開く」を押しページを見てみましょう。
スクリーンショット 2019-04-12 9.11.07.png
おそらく以下の画面になるかと思います。
スクリーンショット 2019-04-12 9.14.52.png

プラグインと依存パッケージのインストール

「プラグイン」→「プラグインを追加する」より「vue-cli-lugin-vuetify」をインストールします。
vuetifyはマテリアルデザインのUIフレームワークです。
依存パッケージも同じように「axios」と「js-cookie」をインストールします。
axiosはHTTPクライアント、js-cookieはクッキーを簡単に扱えるようにするものです。
インストールが終わったら以下のようになっているか確認してください。
多分なってないので「lint」→「タスクの実行」をしてください。
スクリーンショット 2019-04-12 12.06.52.png

実装

これで準備は終わったのでコンポーネントを作っていきます。
基本的なことはググってください。
ちなみに僕はわかってないです。
先にsrc/components/HelloWorld.vuesrc/viewsを削除しときます。

コンポーネント

面倒くさいのでSPA全体をカードを使ったレイアウトにします。
<router-view />から他のファイルを読み込んで表示するのでまあこんな感じ。

src/App.vue
<template>
  <v-app>
    <v-content>
      <v-layout justify-center>
        <v-flex xs12 sm7 md5 lg3>
          <v-spacer class="py-4"></v-spacer>
          <v-card class="elevation-3 pa-2 mx-2">
            <router-view />
          </v-card>
        </v-flex>
      </v-layout>
    </v-content>
  </v-app>
</template>

<script>
import './router.js'

export default {
  name: 'App'
}
</script>

App.vueに表示する以下の4つのファイルを作成します。

src/components/
├── NotFound.vue
├── signin.vue
├── signup.vue
└── user.vue

特に説明しないので頑張って理解してください。
そんな難しいことはやってないはずです...
使ってない変数とかありますが後で使います。


signin.vue
signin.vue
<template>
  <div class="signin">
    <v-snackbar color="error" v-model="snackbar" top :timeout="3000">
      {{ snackbarText }}
    </v-snackbar>

    <v-form ref="form">
      <v-card-text class="pb-2">
        <v-layout justify-start>
          <span class="display-1 primary--text mb-2">サインイン</span>
        </v-layout>

        <v-text-field
          class="mb-2"
          type="text"
          prepend-icon="person"
          v-model="username"
          :rules="nameRules"
          label="ユーザ名"
          clearable
          required
          :error="signinError"
        ></v-text-field>

        <v-text-field
          type="password"
          prepend-icon="lock"
          v-model="password"
          :append-icon="show ? 'visibility_off' : 'visibility'"
          :type="show ? 'text' : 'password'"
          @click:append="show = !show"
          :rules="passRules"
          label="パスワード"
          clearable
          required
          :error="signinError"
        ></v-text-field>
      </v-card-text>

      <v-card-actions>
        <v-layout justify-end>
          <v-btn flat color="success" @click="signup">サインアップ</v-btn>
          <v-btn type="submit" depressed color="primary" @click="signin">サインイン</v-btn>
        </v-layout>
      </v-card-actions>
    </v-form>

  </div>
</template>

<script>
import router from '../router.js'

export default {
  name: 'signin',
  data: function () {
    return {
      snackbar: false,
      snackbarText: '',
      username: '',
      password: '',
      nameRules: [v => !!v || 'ユーザ名を入力してください'],
      passRules: [v => !!v || 'パスワードを入力してください'],
      signinError: false,
      show: false
    }
  },
  methods: {
    signin: () => {
      router.push({ name: 'user' })     
    },
    signup: () => {
      router.push({ name: 'signup' })
    }
  }
}
</script>


signup.vue
signup.vue
<template>
  <div class="signup">
    <v-snackbar :color="snackbarColor" v-model="snackbar" top :timeout="3000">
      {{ snackbarText }}
    </v-snackbar>

    <v-card-text>
      <v-layout justify-start>
        <span class="display-1 primary--text mb-2">サインアップ</span>
      </v-layout>
      <v-form ref="form">
        <v-text-field
          class="mb-2"
          type="text"
          prepend-icon="person"
          v-model="username"
          :rules="[rules.required, rules.min3, rules.max20]"
          label="ユーザ名"
          clearable
          required
          :error="!username"
        ></v-text-field>

        <v-text-field
          class="mb-2"
          prepend-icon="lock"
          v-model="password"
          :append-icon="show ? 'visibility_off' : 'visibility'"
          :rules="[rules.required]"
          :type="show ? 'text' : 'password'"
          label="パスワード"
          counter
          required
          @click:append="show = !show"
          :error="!password"
        ></v-text-field>

        <v-text-field
          prepend-icon="lock"
          v-model="passwordConf"
          :rules="[rules.required]"
          type="password"
          label="確認"
          counter
          required
          :error="!passwordConf"
        ></v-text-field>
      </v-form>
    </v-card-text>

    <v-card-actions>
      <v-layout justify-end>
        <v-btn flat color="primary" @click="signin">サインイン</v-btn>
        <v-btn
          depressed
          color="success"
          @click="signup"
          type="submit"
        >
          サインアップ
        </v-btn>
      </v-layout>
    </v-card-actions>

  </div>
</template>

<script>
import router from '../router.js'

export default {
  name: 'signup',
  data: function () {
    return {
      snackbar: false,
      snackbarText: '',
      snackbarColor: '',
      username: '',
      password: '',
      passwordConf: '',
      show: false,
      rules: {
        required: v => !!v || '必須項目です',
        min3: v =>
          (v == null ? '' : v).length >= 3 || '3文字以上で入力してください',
        max20: v =>
          (v == null ? '' : v).length <= 20 || '20文字以内で入力してください'
      }
    }
  },
  methods: {
    signup: () => {
      router.push({ name: 'signin' })    
    },
    signin: () => {
      router.push({ name: 'signin' })
    }
  }
}
</script>


user.vue
user.vue
<template>
  <div class="user">

    <v-card-text>
      <v-layout justify-start>
        <span class="display-1 primary--text mb-2">ユーザ</span>
      </v-layout>
      {{ username }}
    </v-card-text>

    <v-card-actions>
      <v-layout justify-end>
        <v-btn
          depressed
          color="error"
          @click="signout"
        >
          サインアウト
        </v-btn>
      </v-layout>
    </v-card-actions>

  </div>
</template>

<script>
import router from '../router.js'

export default {
  name: 'user',
  data: function () {
    return {
      username: ''
    }
  },
  methods: {
    signout: () => {
      router.push({ name: 'signin' })
    }
  }
}
</script>


NotFound.vue
NouFound.vue
<template>
  <div class="NotFound">

    <v-card-text>
      <v-layout justify-start>
        <span class="display-1 primary--text mb-2">NotFound</span>
      </v-layout>
    </v-card-text>

    <v-card-actions>
      <v-layout justify-end>
        <v-btn
          depressed
          color="success"
          @click="signin"
        >
          サインイン
        </v-btn>
      </v-layout>
    </v-card-actions>

  </div>
</template>

<script>
import router from '../router.js'

export default {
  name: 'NotFound',
  methods: {
    signin: () => {
      router.push({ name: 'signin' })
    }
  }
}
</script>

ルーティング

ページ遷移ができるようにルーティングを定義します。

src/components/router.js
import Vue from 'vue'
import Router from 'vue-router'
import Signin from './components/signin.vue'
import Signup from './components/signup.vue'
import User from './components/user.vue'
import NotFound from './components/NotFound.vue'

Vue.use(Router)

export default new Router({
  routes: [
    { path: '/', name: 'root', component: Signin },
    { path: '/signin', name: 'signin', component: Signin },
    { path: '/signup', name: 'signup', component: Signup },
    { path: '/user', name: 'user', component: User },
    { path: '*', name: 'NotFound', component: NotFound }
  ]
})

確認①

では、ここまでちゃんと作れているか確認します。
コピペしたなら恐らく以下のようになるはずです。
ボタンを押しての移動と、存在しないURLでNouFoundに飛ばされるかを確認しましょう。
スクリーンショット 2019-04-21 22.48.57.png

非同期通信

axiosを使ってAPIサーバへの通信部分を作ります。
<script>部分のみ書き換えてください。

パスワードをハッシュ化してサーバに送ります。
意味あるんですかね。


signin.vue
signin.vue
<script>
import Cookies from 'js-cookie'
import Axios from 'axios'
import crypto from 'crypto'
import router from '../router.js'

export default {
  name: 'signin',
  data: function () {
    return {
      snackbar: false,
      snackbarText: '',
      username: '',
      password: '',
      nameRules: [v => !!v || 'ユーザ名を入力してください'],
      passRules: [v => !!v || 'パスワードを入力してください'],
      signinError: false,
      show: false
    }
  },
  methods: {
    signin: function () {
      this.snackbar = false
      this.signinError = false
      if (!this.$refs.form.validate()) {
        return
      }
      let sha256 = crypto.createHash('sha256')
      sha256.update(this.password)
      const hashPass = sha256.digest('base64')

      let axios = Axios.create({
        baseURL: 'http://localhost:5000',
        headers: {
          'Content-Type': 'application/json',
          'X-Requested-With': 'XMLHttpRequest'
        },
        responseType: 'json'
      })
      let self = this
      axios
        .post(
          '/signin',
          {
            username: self.username,
            password: hashPass
          },
          {
            validateStatus: function (status) {
              return status < 500
            }
          }
        )
        .then(res => {
          if (res.data.access_token) {
            Cookies.set('jwt_token', res.data.access_token)
            router.push({ name: 'user' })
          } else if (res.status === 401) {
            self.snackbarText = 'ユーザ名またはパスワードが違います'
            self.snackbar = true
            self.signinError = true
          } else {
            throw new Error()
          }
        })
        .catch(() => {
          self.snackbarText = 'エラーが発生しました'
          self.snackbar = true
        })
    },
    signup: () => {
      router.push({ name: 'signup' })
    }
  }
}
</script>

バリデーションして問題無かったらパスワードをハッシュ化して両方送ります。


signup.vue
signup.vue
<script>
import crypto from 'crypto'
import Axios from 'axios'
import router from '../router.js'

export default {
  name: 'signup',
  data: function () {
    return {
      snackbar: false,
      snackbarText: '',
      snackbarColor: '',
      username: '',
      password: '',
      passwordConf: '',
      show: false,
      rules: {
        required: v => !!v || '必須項目です',
        min3: v =>
          (v == null ? '' : v).length >= 3 || '3文字以上で入力してください',
        max20: v =>
          (v == null ? '' : v).length <= 20 || '20文字以内で入力してください'
      }
    }
  },
  methods: {
    signup: function () {
      this.snackbar = false
      if (!this.$refs.form.validate()) {
        return
      }
      if (this.password !== this.passwordConf) {
        return
      }

      let sha256 = crypto.createHash('sha256')
      sha256.update(this.password)
      const hashPass = sha256.digest('base64')

      let sha256Conf = crypto.createHash('sha256')
      sha256Conf.update(this.passwordConf)
      const hashPassConf = sha256Conf.digest('base64')

      let axios = Axios.create({
        baseURL: 'http://localhost:5000',
        headers: {
          'Content-Type': 'application/json',
          'X-Requested-With': 'XMLHttpRequest'
        },
        responseType: 'json'
      })
      let self = this
      axios
        .post('/signup', {
          username: self.username,
          password: hashPass,
          passwordConf: hashPassConf
        })
        .then(res => {
          self.snackbarText = '登録しました'
          self.snackbarColor = 'success'
          self.snackbar = true
          setTimeout(function () {
            router.push({ name: 'signin' })
          }, 1500)
        })
        .catch(() => {
          self.snackbarText = 'エラーが発生しました'
          self.snackbarColor = 'error'
          self.snackbar = true
        })
    },
    signin: () => {
      router.push({ name: 'signin' })
    }
  }
}
</script>

beforeMountでレンダリング(?)される前にトークンが不正でないか確認する感じです。


user.vue
user.vue
<script>
import Cookies from 'js-cookie'
import Axios from 'axios'
import router from '../router.js'

export default {
  name: 'user',
  data: function () {
    return {
      username: ''
    }
  },
  beforeMount: function () {
    let token = Cookies.get('jwt_token')
    let axios = Axios.create({
      baseURL: 'http://localhost:5000',
      headers: {
        'Content-Type': 'application/json',
        Authorization: 'Bearer ' + token
      },
      responseType: 'json'
    })
    let self = this
    axios
      .get('/protected')
      .then(res => {
        if (res.status === 200) {
          let data = res.data
          self.username = data.username
        } else {
          Cookies.remove('jwt_token')
          router.push({ name: 'signin' })
        }
      })
      .catch(() => {
        Cookies.remove('jwt_token')
        router.push({ name: 'signin' })
      })
  },
  methods: {
    signout: () => {
      Cookies.remove('jwt_token')
      router.push({ name: 'signin' })
    }
  }
}
</script>

確認②

まだ、バックエンドを作ってないので、スナックバーが表示されるかだけ確認してみましょう。

バックエンド

Flaskを使ってAPIサーバを作っていきます。
今回はログイン、新規登録、トークン確認(認証?)の3種類です。

準備

pipでライブラリをインストールし、ディレクトリ構造通りにファイルを作成します。
今回の規模だとファイルを分割する必要はないですが、拡張性を考えてこの構造にしてます。

ライブラリ一覧
flask
flask_cors
flask_jwt_extended
MySQLdb
bcrypt
ディレクトリ構造
api/
├── views/
│   ├── __init__.py
│   ├── auth.py
│   └── test.py
├── config.py
├── main.py
└── module.py

また、以下のSQL文でDBにテーブルを作成します。

SQL文
CREATE TABLE `users_data` (
  `id` int(3) NOT NULL AUTO_INCREMENT,
  `username` varchar(20) NOT NULL,
  `password` varchar(60) NOT NULL,
  `jti` varchar(36) DEFAULT NULL,
  PRIMARY KEY (`id`)
);

実装

viewsからBlueprintをインポートしてFlaskを起動するようにします。
デバッグはなんとなくオンです。

main.py
#!/usr/bin/python3.6
from config import *
from flask import Flask
from flask_jwt_extended import JWTManager
from flask_cors import CORS
from views.auth import *
from views.test import *

# API設定
app = Flask(__name__)
jwt = JWTManager(app)
CORS(app)

# views読み込み
app.register_blueprint(auth)
app.register_blueprint(test)

if __name__ == "__main__":
    app.run(debug=True)

SQL文の実行とトークンが最新のものであるか確認するモジュールです。
DBの操作はよくわからなかったのでこうしてますが、楽な方法がありそうです。

module.py
import MySQLdb
from MySQLdb.cursors import DictCursor

# SQL文送信用
def db(sql, data=None):
    db = MySQLdb.connect(host="localhost", user="root", passwd="password", db="sample", charset="utf8")
    cur = db.cursor(DictCursor)
    cur.execute(sql, data)
    rows = cur.fetchall()
    cur.close()
    db.commit()
    db.close()
    if rows:
        return rows[0]

# 古いトークンの使用禁止
def auth_jti(id, token_jti):
    sql = "SELECT display_name, username, jti FROM users_data WHERE BINARY id=%s"
    user = db(sql, [ id ])
    if token_jti == user["jti"]:
        return { "username": user["username"] }
    return False

モジュールを読み込みやすくするファイル?
というかブループリントの場合は必須なのかな...

views/__init__.py
from flask import Blueprint

auth = Blueprint("auth", __name__)
test = Blueprint("test", __name__)

肝心のAPI部分です。

views/auth.py
from . import auth
from flask import jsonify, request
from flask_jwt_extended import (
    jwt_required, create_access_token,
    get_jwt_identity, get_jti, get_raw_jwt
)
from module import db, auth_jti
import bcrypt
import time

@auth.route("/signin", methods=["POST"])
def signin():
    username = request.json.get("username", None)
    password = request.json.get("password", None)
    if not username or not password:
        return jsonify( {"message": "Format does not match"} ), 400

    sql = "SELECT id, username, password FROM users_data WHERE BINARY username=%s"
    try:
        user = db(sql, [ username ])
        if not user:
            return jsonify( {"message": "Bad username or password"} ), 401
        if bcrypt.checkpw(password.encode(), user["password"].encode()):
            sql = "UPDATE users_data SET updated_at=%s WHERE username=%s"
            timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
            db(sql, [ timestamp, username ])
        else:
            return jsonify( {"message": "Bad username or password"} ), 401
    except Exception as e:
        return jsonify( {"message": "An error occurred"} ), 500

    access_token = create_access_token(identity=user["id"])
    sql = "UPDATE users_data SET jti=%s WHERE username=%s"
    db(sql, [ get_jti(access_token), username ])
    return jsonify(access_token=access_token), 200

@auth.route("/protected", methods=["GET"])
@jwt_required
def protected():
    user = auth_jti(get_jwt_identity(), get_raw_jwt()["jti"])
    if not user:
        return jsonify( {"message": "Bad access token"} ), 401
    return jsonify( {"username": user["username"]} ), 200

@auth.route("/signup", methods=["GET", "POST"])
def signup():
    if not request.is_json:
        return jsonify( {"message": "Missing JSON in request"} ), 400
    data = request.get_json()
    username = data["username"]
    password = data["password"]
    password_conf = data["passwordConf"]

    if username and username.encode().isalnum() and password != password_conf:
        return jsonify( {"mode": "signup", "status": "error", "message": "Format does not match"} ), 400

    sql = "SELECT * FROM users_data WHERE BINARY username=%s"
    if db(sql, [ username ]):
        return jsonify( {"mode": "signup", "status": "error", "message": "This username cannot be used"} ), 400
    else:
        salt = bcrypt.gensalt(rounds=10, prefix=b"2a")
        hashed_pass = bcrypt.hashpw(password.encode(), salt).decode()
        sql = "INSERT INTO users_data (username, password) VALUE (%s, %s)"
        db(sql, [ username, hashed_pass ])
        return jsonify( {"mode": "signup", "status": "success", "message": "Completed"} ), 200

きちんと分割できてるか確認するファイルです。

views/test.py
from . import test

@test.route("/")
def root():
    return "Hello World!"

実行

以下コマンドで実行してみましょう。

$ python3.6 main.py

http://localhost:5000にアクセスしHello World!と表示されるか確認します。
それができたら実際に新規登録やサインインなどもできるはずです!

終わりに

あんま説明ないですけどなんとなく雰囲気は伝わったと信じてます。
それとFlask-JWT-Extended'sを使った日本語の記事とかってこれが初なのでは...?
使いやすいけどそもそもJWTとかFlaskを使ってないんですかね。

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

[備忘録]Vueファイル+Prettier(vscode)

VueファイルをPrettierで綺麗にしたい

.Vueファイルを保存した時に、Prettierでいい感じに整えてほしい

まずESLintを設定する

setting.json

{
  "editor.formatOnSave": true,
  "eslint.validate": ["javascript", { "autoFix": true, "language": "vue" }]
}

インストール

npm add --dev eslint eslint-config-standard eslint-plugin-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node
npm add --dev eslint-plugin-vue

.eslintrc.js作成

module.exports = {
  root: true,
  extends: ['standard', 'plugin:vue/essential'],
  plugins: ['vue'],
}

vueファイルでエラーめっちゃ出た
image.png

:coffee: :coffee: :coffee:

エラー消すべくPrettierを適応

今エラーが出ている個所をなおしてもらいますー

ESLint のフォーマット機能を Prettier に変更するといいらしい
インストール

npm add --dev prettier eslint-config-prettier eslint-plugin-prettier

.eslintrc.jsの変更

module.exports = {
  root: true,
  extends: ['standard', 'plugin:vue/essential', 'plugin:prettier/recommended'],
  plugins: ['vue'],
  rules: {
    'prettier/prettier': [
      'error',
      {
        semi: false,
        singleQuote: true,
        trailingComma: 'es5',
      },
    ],
  },
};

ここまででの設定でESLintとPrettierが競合している・・・???
Prettierで直してもsaveしたら元に戻るという事態が発生

setting.jsonに以下追加したらうまくいったよ

"prettier.eslintIntegration": true,
"eslint.autoFixOnSave": true,

(参考)

https://t-kojima.github.io/2018/06/22/0018-using-eslint-prettier-in-vue/

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

[備忘録]vue-cli3でVue.js

vue.js

学習コストが低いらしいのでやってみた
個人的にtypescriptでやるのむずくない??と思いました

vue-cli3でプロジェクト作る

まずインストールするよ(2.xが入ってる場合はアンインストールしてからインストールする)

npm install -g @vue/cli

作るよ

vue create vue-test-project

Manualを選択して作る(全部選択)

Vue CLI v3.5.5
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)Babel, TS, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript for auto-detected polyfills? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
? Pick a linter / formatter config: Prettier
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save, Lint and fix on commit
? Pick a unit testing solution: Jest
? Pick a E2E testing solution: Nightwatch
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In package.json
? Save this as a preset for future projects? No

雰囲気でやった:ghost:

typescriptやるならUse class-style component syntax? Yes
ESLintもtsに対応してるぽい
node-sassを使う場合にはnode.jsに依存するので注意が必要

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

Nuxt.jsプロジェクトをCloud Runに乗せてデプロイするぜ!(GCPの新しいサービス)

Google Cloud Next'19で新しい発表がありましたね
AWS一筋の人は浮気したくなるじゃない?^^(クラウド戦争の種を撒こう)
image.png

Cloud Runとは

Google Cloud Next’19で発表された新サービス
https://cloud.withgoogle.com/next/sf

コンテナ単位でサーバーレスの実行環境です。一番の目的としてはこれだと思います。
あらゆる言語で書かれたカスタムアプリケーションのサーバーレスでの運用、そして多様な環境にまたがる互換性の確保を目的としている。
ほほっ!!!!

使うもの

Nuxt.js (SSR必須) Docker (クジラかな?)

gcloud (gcpのcli)

GCPのサービス:
Cloud Run (今回の主役) Container Registry (GCPのプロジェクトのコンテナリポジトリ管理)

1.Nuxt.jsプロジェクト作成

image.png
まずはNuxt.jsのプロジェクト設定ですね。
Choose rendering modeはUniversal以外には好きなの選んで大丈夫です。

npx create-nuxt-app nuxt-cloudrun
> Generating Nuxt.js project in /Users/jakushin.cho/Projects/nuxt-cloudrun
? Project name nuxt-cloudrun
? Project description My dandy Nuxt.js project
? Use a custom server framework express
? Choose features to install (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Use a custom UI framework vuetify
? Use a custom test framework none
? Choose rendering mode Universal  <--- SSR 
? Author name jakushin
? Choose a package manager npm

ローカルで起動して、問題はないかをチェックしましょう

npm run dev

Vuetify + Nuxt.jsのデフォルト画面
スクリーンショット 2019-04-11 9.56.00.png

2.Dockerファイルの設定

image.png

プロジェクトディレクトリでdockerfileを作成します、内容はこちら

FROM node:10

WORKDIR /usr/src/app

ENV PORT 8080
ENV HOST 0.0.0.0

COPY package*.json ./

RUN npm install --only=production

# Nuxtプロジェクトのコードをコンテナにコピー
COPY . .

# prodビルト、サーバ起動
RUN npm run build
CMD npm start

ファイル作成後、以下のコマンドでdocker image作成

> docker build ./
..
..
..
(ログ省略)
 ---> 317bd77abc49
Successfully built 317bd77abc49  <--- 作成したイメージID

作成したイメージIDで、docker runでnuxtのアプリを起動チェック(スキップ可)

> docker run -p 8080:8080 317bd77abc49
nuxt-cloudrun@1.0.0 start /usr/src/app
cross-env NODE_ENV=production node server/index.js
01:13:09  READY  Server listening on http://0.0.0.0:8080

起動後、0.0.0.0:8080にアクセスし、先程のvuetify+nuxtの画面が表示されたらOK!

動いているコンテナを消し忘れずですね。

> docker kill be0d7f77e431

3.Container Registry (GCP)にコンテナイメージをpush

image.png
GCPでプロジェクトを作成しのが前提です。Cloud Container Registryを有効にしてください。

まずdockerイメージにtag付けましょう。分かりやすくGCPのContainer Registryに統一しましょう。

                   GCPのコンテナホスト名   プロジェクト名    dockerイメージ名(任意)
> docker tag 317bd77abc49 [us.gcr.io]/[myprojectid]/[nuxt-cloudrun]

push先は同じURLでいいので、GCPのコンテナにpush!!!!

> docker push [us.gcr.io]/[myprojectid]/[nuxt-cloudrun]

成功したらリポジトリに追加されます。
スクリーンショット 2019-04-11 10.35.06.png

4.Cloud Run Beta(GCP)

有効にしてくださいね。
CREATE SERVICEをクリックしてで新規作成しましょう。
スクリーンショット 2019-04-11 10.36.57.png

サービス作成画面でコンテナイメージのURLの右の「選択」ボタンをクリックします。
スクリーンショット 2019-04-11 10.40.08.png

Container Registryに上げたnuxt-cloudrunイメージを選びます。
スクリーンショット 2019-04-11 10.40.26.png

一般のユーザに公開したいならば、認証の未認証の呼び出しを許可を選び、「作成」ボタンをクリックします
スクリーンショット 2019-04-11 10.45.12.png

成功した画面です
ログの方の出力はlocalのnuxt.jsのプロジェクトのログが一致しているの確認できます。
スクリーンショット 2019-04-11 10.51.48.png

やったー!コンテナをサーバーレスにできたぜ!!!

日本語が下手くそな僕がまとめようとしても、まとまれないのまとめ

コンテナをサーバーレス化にして、ますますマイクロサービスの時代の流れを感じましたね!
以上!(無責任)
..
..
もう少し書きます。
cloud functionでサーバーレス、cloud runでコンテンをサーバーレス
インフラのことを気にしたくなければ、GCPは凄くいい選択肢ではないか。

料金はこちら
https://cloud.google.com/run/pricing
試しで作ったサービスの消し忘れを注意してくださいね。

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