- 投稿日:2019-04-11T23:52:45+09:00
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_taskとits_dueに情報が入っていることを確認し,それらの情報をもとに新しいtodoのオブジェクトを生成した上で,それをtodosに追加し,最後にnew_taskとits_dueを空に戻している.なお,
new_taskとits_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.bodyをjson.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
- 投稿日:2019-04-11T23:36:24+09:00
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側に送ってデータベースを更新することを試す.
- 投稿日:2019-04-11T22:38:53+09:00
インスタンスの外からメソッドを呼ぶ
初投稿です。インスタンスの外からメソッドを呼びたいときがあります。
Vue インスタンスはマウントした要素の
__vue__プロパティにセットされるそうなので、例えば次のようなコンポーネントがあったとき<template> <div id="awesome-element" /> </template> <script> export default { methods: { awesomeMethod () { // ... }, }, } </script>次のようにメソッドを呼ぶことができます。
document.querySelector('#awesome-element').__vue__.awesomeMethod()これがいつ必要になるんだという話ですが、WKWebView とか WebView とかそういう事情があります。
- 投稿日:2019-04-11T21:01:08+09:00
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(シングルページアプリケーション)にするためです。
ヒストリーモードはオフのままにしておき、linterは適当に「Standard config」を選んで作成します。
プリセットを保存するか聞かれるので念のため保存しておきます。以下の画面で「タスクを実行」→「アプリを開く」を押しページを見てみましょう。
おそらく以下の画面になるかと思います。
プラグインと依存パッケージのインストール
「プラグイン」→「プラグインを追加する」より「vue-cli-lugin-vuetify」をインストールします。
vuetifyはマテリアルデザインのUIフレームワークです。
依存パッケージも同じように「axios」と「js-cookie」をインストールします。
axiosはHTTPクライアント、js-cookieはクッキーを簡単に扱えるようにするものです。
インストールが終わったら以下のようになっているか確認してください。
多分なってないので「lint」→「タスクの実行」をしてください。
実装
これで準備は終わったのでコンポーネントを作っていきます。
基本的なことはググってください。
ちなみに僕はわかってないです。
先にsrc/components/HelloWorld.vueとsrc/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.jsimport 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に飛ばされるかを確認しましょう。
非同期通信
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.pyimport 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__.pyfrom flask import Blueprint auth = Blueprint("auth", __name__) test = Blueprint("test", __name__)肝心のAPI部分です。
views/auth.pyfrom . 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.pyfrom . import test @test.route("/") def root(): return "Hello World!"実行
以下コマンドで実行してみましょう。
$ python3.6 main.py
http://localhost:5000にアクセスしHello World!と表示されるか確認します。
それができたら実際に新規登録やサインインなどもできるはずです!終わりに
あんま説明ないですけどなんとなく雰囲気は伝わったと信じてます。
それとFlask-JWT-Extended'sを使った日本語の記事とかってこれが初なのでは...?
使いやすいけどそもそもJWTとかFlaskを使ってないんですかね。
- 投稿日:2019-04-11T20:41:10+09:00
[備忘録]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'], }
![]()
![]()
エラー消すべく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/
- 投稿日:2019-04-11T20:40:33+09:00
[備忘録]vue-cli3でVue.js
vue.js
学習コストが低いらしいのでやってみた
個人的にtypescriptでやるのむずくない??と思いましたvue-cli3でプロジェクト作る
まずインストールするよ(2.xが入ってる場合はアンインストールしてからインストールする)
npm install -g @vue/cli作るよ
vue create vue-test-projectManualを選択して作る(全部選択)
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雰囲気でやった
typescriptやるなら
Use class-style component syntax? Yes
ESLintもtsに対応してるぽい
node-sassを使う場合にはnode.jsに依存するので注意が必要
- 投稿日:2019-04-11T11:02:53+09:00
Nuxt.jsプロジェクトをCloud Runに乗せてデプロイするぜ!(GCPの新しいサービス)
Google Cloud Next'19で新しい発表がありましたね
AWS一筋の人は浮気したくなるじゃない?^^(クラウド戦争の種を撒こう)
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プロジェクト作成
まずは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 dev2.Dockerファイルの設定
プロジェクトディレクトリで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 be0d7f77e4313.Container Registry (GCP)にコンテナイメージをpush
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]4.Cloud Run Beta(GCP)
有効にしてくださいね。
CREATE SERVICEをクリックしてで新規作成しましょう。
サービス作成画面でコンテナイメージのURLの右の「選択」ボタンをクリックします。
Container Registryに上げたnuxt-cloudrunイメージを選びます。
一般のユーザに公開したいならば、認証の未認証の呼び出しを許可を選び、「作成」ボタンをクリックします
成功した画面です
ログの方の出力はlocalのnuxt.jsのプロジェクトのログが一致しているの確認できます。
やったー!コンテナをサーバーレスにできたぜ!!!
日本語が下手くそな僕がまとめようとしても、まとまれないのまとめ
コンテナをサーバーレス化にして、ますますマイクロサービスの時代の流れを感じましたね!
以上!(無責任)
..
..
もう少し書きます。
cloud functionでサーバーレス、cloud runでコンテンをサーバーレス
インフラのことを気にしたくなければ、GCPは凄くいい選択肢ではないか。料金はこちら
https://cloud.google.com/run/pricing
試しで作ったサービスの消し忘れを注意してくださいね。
















