Angularの修行
最近、Angularの修行をしています。
Angularの公式チュートリアルといえばTour of Herosですが、Angularの日本語ドキュメントは文章表現、言い回しが独特でとっつきにくいところがあります。
ちょっと肩慣らしを思って、以下のサイトのチュートリアルをやりました。
Angular入門 - とほほのWWW入門
こちらではAngular以外にもBootstrap等も判りやすく解説されていて、結構お世話になっているサイトです。
ちなみにチュートリアル内容をそのまま写経していると、おそらくAngularのバージョン差分に引っかかり、ちょいちょいTypeScriptのトランスコードでエラーになります。そのあたりをググってStackOverflowとかにたどり着きながら解決するのも良い勉強になりますが、なかなかにシンドいですね。
で、チュートリアルを進めていくと、最後のところでAPIサーバーを立てる必要が出てきます。
チュートリアルではnodejsでAPIサーバーを立てているのですが、自分はイマイチnodejsに噛み合わず、pythonでAPIサーバーを作りたいので、チュートリアルに載っているのとほぼ同等の機能をpython + flaskで実装しました。
Access-Control-Allow-Originヘッダが無い?
チュートリアルではAngularのアプリのコードを提供するAPサーバーと、アプリにデータを提供するAPIサーバーが別のポート番号で動作します。
クロスサイトスクリプティング対策に対応するために、サンプルのAPIサーバーでは「Access-Control-Allow-Origin」ヘッダに対して「*」を付けて返す用に実装されていました。
これを真似てFlask版のAPIサーバーでも、下記の様にGETやPUTのレスポンスに個別にヘッダを追加して対処していました。
(add_response_header()でレスポンスに「Access-Control-Allow-Origin」ヘッダと「Access-Control-Allow-Headers」ヘッダを追加)
# Response Headers res_headers = [ {'header':'Access-Control-Allow-Origin','value':'*'}, {'header':'Access-Control-Allow-Headers','value':'Origin, X-Requested-With, Content-Type, Accept'} ] def add_response_header(response): global res_headers for header in res_headers: response.headers[header['header']] = header['value'] # Routing @app.route('/users',methods=['GET','POST']) def f_users(): global users if request.method == 'GET': response = jsonify(users) add_response_header(response) return response elif request.method == 'POST': # 書き込まれたjsonの内容チェックをするべきだが省略 json = request.get_json() users.append(json) response = Response(status = 200) add_response_header(response) return response
このコードをcurlでテストして想定通りのヘッダが付与されている事を確認、その上でAngularアプリと連動させる実験をしました。
ところが...GETは良いのですが、データをAPIサーバーに書き込もうとすると正常に動作しない。
Chrome上でアプリを動かしていたのですが、Chromeのデバッグ表示を有効にして見てみるとCORSエラーとして「Access-Control-Allow-Origin」が無いという表示が出てエラーになっていました。
いや、ヘッダ返しているはずだけど?と思ってAPIサーバー側のログを見ると、POSTではなく、OPTIONSメソッドが発行されています。
どうやら(少なくとも)Chrome上では、AngularのhttpモジュールからPOSTしようとすると、POSTの前でOPTIONSメソッドでヘッダチェックが走り、CORS(Cross Origin Resource Sharing: オリジン間リソース共有)のチェックをパスすると、POSTメソッドが発行される様です。
ということは、APIサーバー側でOPTIONSメソッド対応も書かなきゃいけないの?めんどせ〜と思ってしまいました。
flask_cors
リソース毎にOPTIONSメソッド対応も一個一行書くのは超面倒だと思って調べたら、flaskにはflask_corsというオプションパッケージがあることが分かりました。
pipでインストール出来ます。
$ pip install flask-cors
一番単純な使い方は公式ドキュメントの先頭に載っていますが、
from flask import Flask from flask_cors import CORS app = Flask(__name__) CORS(app)
と、CORS()という関数をappを渡すだけ。これでAPIサーバーの全ての応答に「Access-Control-Allow-Origin」ヘッダが付与されます。
ちなみにデフォルトでは「*」が値に設定されますが、CORS()のオプションでオリジンのlistを渡す事が可能です。
全てのリソースにではなく、リソース毎にCORSの許可・不許可を切り替えたい場合は、各リソースのルーティングディレクティブの下に@cross_origin()というディレクティブを並べる事で、個別にヘッダを足さなくても大丈夫な様です。
@app.route("/") @cross_origin()
なお、
Angular入門 - とほほのWWW入門
に対応したPython+Flask版のAPIサーバー全ソースを以下に掲載しておきます。
(別のソースからコピペ流用したところがあるので、結構不要なimportが混ざっています)
エラーチェック等は全然していません。例えばAngularアプリ側からPOSTされたjsonのフォーマットをチェックする等、本来はすべきですが、受信したものを信じて処理しています。
#!/usr/bin/env python # -*- coding: utf-8 -*- from flask import Flask, Response, render_template, request, redirect, url_for,jsonify,make_response from werkzeug.serving import WSGIRequestHandler from flask_cors import CORS import json # Port No. PORT=5000 # Internal Data(Example) users = [ { 'id':1, 'name':'Yamada','email': 'yamada@example.com'}, { 'id':2, 'name':'Tanaka','email': 'tanaka@example.com'}, { 'id':3, 'name':'Suzuki','email': 'suzuki@example.com'} ] def search_userid(id): global users idx = 0 ret_idx = -1 for user in users: if user['id'] == id: ret_idx = idx break idx += 1 return ret_idx # Initialize APP app = Flask(__name__) # JSONにASCII以外の文字列を使用可能にする。(このサンプルでは意味なし) app.config["JSON_AS_ASCII"] = False # CORS(Cross Origin Resource Sharing: オリジン間リソース共有)設定 CORS(app) # Routing @app.route('/users',methods=['GET','POST']) def f_users(): global users if request.method == 'GET': response = jsonify(users) return response elif request.method == 'POST': # 書き込まれたjsonの内容チェックをするべきだが省略 json = request.get_json() users.append(json) response = Response(status = 200) return response @app.route('/users/<int:id>',methods=['GET','POST','DELETE']) def f_users_id(id): global users idx = search_userid(id) if request.method == 'GET': if idx != -1: response = jsonify(users[idx]) else: response = Response(status = 404) return response elif request.method == 'POST': # 本来はURLで指定されたidとjson内のidを比較チェックすべきだが省略 # 書き込まれたjsonの内容チェックをするべきだが省略 json = request.get_json() response = jsonify(json) if idx != -1: users[idx] = json else: response.status = 404 return response # 書き込まれたjsonの内容チェックをするべきだが省略 json = request.get_json() response = jsonify(json) if idx != -1: users[idx] = json else: response.status = 404 return response elif request.method == 'DELETE': if idx != -1: del users[idx] response = Response(status = 200) else: response = Response(status = 404) return response if __name__ == '__main__': WSGIRequestHandler.protocol_version = "HTTP/1.1" app.debug = False app.run(host='0.0.0.0',port=PORT)