目標(私的な)
若干CORSだけに絞っていないので、目的が分散してしまっていますが、次のことをここでは学びます(もちろん僕がw)。
- まずはCORSをしっかり理解する(ついでにOPTIONSなども)。
- 詳細は避けるけれど、Angular7の標準的な流儀でサンプルを作る(ただし、詳細はAngularは主題ではないので、割愛します)。
- json-serverを使って、簡単なJSONを返すサーバの作成。
そもそも
そもそも、CORSが何に関係してくるか(知っている人が多いでしょうが・・・)?
CORSはCross-Origin Resource Sharing(オリジン間リソース共有)の略で、XMLHttpRequest(XHR)や最近使われるようになってきた新しいAPIであるfetch AP等の、古めの言い方ですがいわゆる「Ajax」通信する相手のURLがSame-Origin Policy(同一生成元ポリシー)という一種の制約で、ホスト、スキーム、ポート(これら3つ同じだと同一オリジンと言います)が現在ブラウザーが参照しているものに一致していないと(同一オリジンではないと)通信を許可しないというブラウザー側の制約を回避する方法ということです。
より具体的に見ていくと、まず同一オリジンとは次のようなものを指します。
- http://sakai.com
- http://sakai.com/example
- http://sakai.com:80/example/api
スキーム(上ではhttp)、ホスト(上ではsakai.com)、ポート(上では80)が同じなので全て同一オリジンと言うことになります。したがって、上記のどれかをブラウザーで参照中、その参照中のページから3つどれにでもAjax通信できるということになります。
逆に次のようなものは全て違うオリジンと言うことになります。
- http://sakai.com
- https://sakai.com/example
- http://sakai.com:8080/example/api
- http://chiku.jp:8080/example/api
なので、上の1から3にAjax通信しようとしても、Same-Origin Policyのため、そのままではブラウザーがエラーを出してしまって通信できません。
以前は、JSONPという若干トリッキーな方法でそれを回避することも行われていましたが、現在はセキュリティ的にダメという風潮になり、ここにCORSが登場します。
やや具体的に
下の図のように、ブラウザーでユーザーがAというサーバーにあるWWWサーバにアクセスしあるページを見ます。そのとき、そのページからデータをBというサーバーからデータをもらってくる、これが今回の風景です。登場人物は3人です(ブラウザ、サーバーA、サーバーB)。
Access to XMLHttpRequest at ‘http://localhost:4567/users’ from origin ‘http://localhost:4200’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
確認のためサーバーを構築
ここからはこれまで勉強したことの復習・確認のために、サーバーを構築します。ついでに、Angular7を少し試してみます。
2019年3月28日現在、筆者の環境はMacOS Mojave 10.14.3で、Nodeは11.9.0、Rubyは2.3.3です。この環境でAngular(Node)のインストールとjson-server のインストールをします(すでにある場合は、もちろん、いりません)。Angularはご存じSPAのためのフレームワーク、Sinatoraはサーバーサイド(つまりAjax通信する相手側)のフレームワーク(Railsよりとっても簡単!)。
Angular
ほとんどAngularは追えていないのですが、最近ではこのCLI(コマンドライン)をインストールして始めるのが流儀のようです。
angular/cli
一応バージョンの確認。
サンプルアプリを自動生成
これで、どこかのディレクトリで
を実行します。途中2つ聞かれたので、最初がY、次がCSSを選択しました。出来たディレクトリーにカレントを変えて、そこにあるpakcage.jsonを見ると、どうやらAngularのバージョンは7.2.0のようです(2019年3月28日現在)。
次のようなディレクトリやファイルが作成されていると思います(node_modulesは入れていません)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
├── README.md
├── angular.json
├── e2e
│ ├── protractor.conf.js
│ ├── src
│ │ ├── app.e2e-spec.ts
│ │ └── app.po.ts
│ └── tsconfig.e2e.json
├── package-lock.json
├── package.json
├── src
│ ├── app
│ │ ├── app-routing.module.ts
│ │ ├── app.component.css
│ │ ├── app.component.html
│ │ ├── app.component.spec.ts
│ │ ├── app.component.ts
│ │ └── app.module.ts
│ ├── assets
│ ├── browserslist
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.ico
│ ├── index.html
│ ├── karma.conf.js
│ ├── main.ts
│ ├── polyfills.ts
│ ├── styles.css
│ ├── test.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.spec.json
│ └── tslint.json
├── tsconfig.json
└── tslint.json
上記のmy-appディレクトリーで、
を実行して、ブラウザーでhttp://localhost:4200/にアクセすると次のような画面が表示されるとOKです。
Ajax通信のための設定
Moduleの呼び込み
上記のsrc/app/app.module.tsをエディタで開くと下のようになっています。これはAngularで必要なモジュールを宣言しているところになります。
my-app/src/app/app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Ajax通信するためのモジュールを追加します(正確な言い方にすると「HttpClientModuleをAppModuleにインポートし、後からDI出来るようにしておく」かな?)。
上記の3行目と13行目に、下のようにHttpClientModule関係を追加します(3行目、15行目)。
my-app/src/app/app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
モデルの作成
一応オブジェクト指向なので、APIサーバーから返ってきたJSONの値をモデルに入れ込むために、そのモデルのクラスを作成します。
my-app/src/app/model/user.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
export class User {
public id: number;
public name: string;
public gender: string;
public age: number;
constructor(id: number, name: string, gender: string, age: number) {
this.id = id;
this.name = name;
this.gender = gender;
this.age = age;
}
}
Serviceの作成
そして、Angularの流儀に則って、通信するためのサービスを登録します(サービスを通じず直接書くことも可能です)。
次のようなファイルを作成します。
my-app/src/app/service/http.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {Injectable} from '@angular/core';
import {HttpClient, HttpResponse} from '@angular/common/http';
import {Observable} from 'rxjs';
import {User} from '../model/user';
@Injectable()
export class CorsTestService {
constructor(private http: HttpClient) {
}
public getUsers(): Observable<HttpResponse<User[]>> {
const url = 'http://localhost:4567/users';
return this.http.get<User[]>(url, {observe: 'response'});// observeオプションを付けると、レスポンス全部にアクセス出来るようになるらしい
}
}
}
これは、http://localhost:4567/users にgetでアクセスして、返ってきたJSONをUserの配列で返すメソッドgetUsers()を定義しています。
トップ画面にユーザーを取得するボタンとユーザーを表示する表の追加
次のような9行目から24行目までを挿入します。
ボタンにonclickイベントで実行されるメソッドをgetUsers()→app.component.tsに実装しています。そして、そのapp.component.tsのインスタンス変数であるusersの配列が空じゃなくなったら、挿入した後半の表のfor分が実行されて、表にユーザーが表示される準備をしています。
my-app/src/app/app.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<div style="text-align:center">
<h1>
Welcome to !
</h1>
<img width="300" alt="Angular Logo"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>
<button (click)='getUsers()'>ユーザーの一覧表示</button>
<table border="1" style="width: 33%">
<tr>
<th>Id</th>
<th>氏名</th>
<th>性別</th>
<th>年齢</th>
</tr>
<tr *ngFor="let user of users">
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
<h2>Here are some links to help you start: </h2>
<ul>
<li>
<h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://angular.io/cli">CLI Documentation</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
</li>
</ul>
<router-outlet></router-outlet>
デフォルトのトップ画面はapp/app.component.htmlで定義しているので、ここに書き込みます。
ボタンに仕込んだメソッドにサービスを仕込む
デフォルトのトップ画面はapp/app.component.tsにメソッド等を仕込むので、ここに上記でonclickに書き込んだgetUsers()を定義します。
元々のは下のように単純なものでした。
my-app/src/app/app.component.ts
1
2
3
4
5
6
7
8
9
10
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'tmp-app';
}
これに以下のように追記します。
my-app/src/app/app.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import {Component} from '@angular/core';
import {CorsTestService} from './service/http.service';
import {User} from './model/user';
import {HttpResponse} from '@angular/common/http';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'my-app';
users = new Array<User>();
userService: CorsTestService;
constructor(userService: CorsTestService) {
this.userService = userService;
}
getUsers() {
this.userService.getUsers().subscribe((response: HttpResponse<any>) => {
this.users = response.body.users.map(user => {
return new User(
user.id,
user.name,
user.gender,
user.age
);
});
});
}
}
sinatraのインストール
Angularのプロジェクトとは関係ないところにtest-apiというディレクトリを作成して、そこでsinatraをインストールします。
インストールはこれで終了なので、app.rbというファイルを作成して、次のよう内容を書き込みます(Angularよりとっても単純!)。単純に、getメソッドで/usersにアクセスしたら(ブラウザで開く場合はget)JSON.pretty_generate(users)
を返す、というものです。
test-api/app.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
require 'sinatra'
require 'json'
get '/users' do
content_type :json
users ={
"users": [
{"id": 1, "name": "さかいかずろう", "gender": "男", "age": "20"},
{"id": 2, "name": "ちくともこ", "gender": "女", "age": "21"},
{"id": 3, "name": "伝法谷千代春", "gender": "男", "age": "23"}
]
}
JSON.pretty_generate(users)
end
sinatraはデフォルトのポートが4567なのでhttp://localhost:4567/usersにアクセスすると上記のusersというが表示されればOK。
Access to XMLHttpRequest at ‘http://localhost:4567/users’ from origin ‘http://localhost:4200’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
Preflight requestについて
CORSを理解するにはしっかりブラウザとサーバとのやりとりを理解する必要がありそうです。
今更ですがそもそも、CORSとは何か?
Access-Control-Request-Method、Access-Control-Request-Headers、Origin という3つの HTTP リクエストヘッダを使用するのは OPTIONS です。
プリフライトリクエストは必要に応じてブラウザが自動的に発行します。通常、フロントエンドの開発者はそのようなリクエストを自分で作成する必要はありません。
たとえば、プリフライトリクエストを使用して DELETE リクエストを送信する前に、クライアントが DELETE リクエストを許可するかどうかをサーバに問い合わせている可能性があります。
HTTP OPTIONSメソッドは、ターゲット・リソースの通信オプションを記述するために使用されます。クライアントは、OPTIONSメソッドのURLを指定するか、サーバー全体を参照するアスタリスク(*)を指定することができます。
same-origin restrictions