サァアドベントカレンダーの17日目です!!!
前回の記事とは打って変わって、さくらインターネットのフロントエンド開発についてお話したいと思います。
概要
🎅「いったいにがはじまるんじゃろう?さくらとは???ドキドキが止まらんのじゃ」
さくらのフロントエンドとバックエンド
API設計と疎通
近年さくらインターネットでは、Webアプリケーションフレームワークを利用した SPA(Single Page Application)上で、APIサーバとデータのやり取りを行うことが増えてきました。
例えば、VPSのコントロールパネルで契約サーバの状態を非同期で取得したり、専用サーバの申込画面で、各プランのスペック情報を取得して、サーバのカスタマイズ画面で利用したりと目的は様々です。
SPA という性質上、 webブラウザ上で多くの状態を表現する必要が出てきますので、様々な状態に応じたデータが欲しくなります。このため、フロントエンド側から非同期リクエストで APIサーバからデータを取得するという実装は近年ますます重要度が上がってきました。
制作過程とモックの関係
バックエンドとフロントエンドの仕様に関する情報
ところで、サービス提供までのある過程において、バックエンド側が全てのAPIを実装しているわけではありません。(プロジェクトが始まった当初では、仕様書もできていないのですからなおさらです)
フロントエンドとバックエンドが同期的に実装を進めていく、というのもあまり効率がよくありませんし、スケジュールの遅れの他、バックエンド実装者が事前に把握する情報だけでは作成しにくいAPIもあります。
例えば、バックエンド開発では、フロントエンド側に必要な機能のインタフェース実装を行わないといけませんが、フロントエンド側がどのようなAPIが必要かということを確認しないことには、着手しづらいAPI実装も多いです*。*
かといって、バックエンド側でフロントエンド側の仕様を決定するわけにもいきません。できればあらかじめ要望を満たした仕様を元に実装を行いたいし、フロントエンド側で使いやすい形式のデータを送信してあげたいものです。
そこでこのとき、フロントエンド側ではモックサーバを立てて、ローカル環境で仕様を詰めながら開発を進めていくことがあります。本日はこの部分の実装に関する話を、とくに非同期リクエストを使用した開発を想定して、お話したいと思います。
フロントエンドとAPI
APIモックサーバ構築にあたり、フロントエンド実装者はまず、その時点での仕様書、あるいはワイヤフレーム・デザイン上発生しうる機能から、「この見た目・この機能ならこんなAPIがあってこんな値が返ってくるに違いない」あるいは「ここで POST したらこんな json の値で返してほしいな」というざっくりしたイメージを作ります。
続いて、これを元に Node.js などで動くサーバを用いて、イメージを満たすエンドポイント・データを返す簡易APIモックサーバを構築していきます。
もちろんプロジェクトにより違いはあります。API側がすでにおおまかに実装を終えている場合もありますので、それに合わせてモックサーバにエンドポイントを作成します。
ただし、この場合でも、API側が実装済みの機能に対して、画面側ではこうしたいという要望が出てくる場合には、バックエンド実装者に改修をお願いして、都合の良い形式で返してもらうなどの調整を行ったりします。
アプリケーションの実践
かけあしでしたが、さくらインターネットでのフロントエンド開発におけるモックサーバの立ち位置について説明しました。
では実際に、モックサーバを利用した開発が場合どのように行われているかをイメージしてもらうために、サンプルアプリケーションを作成しながら、説明してみたいと思います。
今回使用するサンプルアプリケーションは Github にあげておきました。必要に応じて御覧ください。
https://github.com/octonyat/myqiita
💡 利用するライブラリ・モジュールのバージョンについて、特に注釈のない限り、package.json に指定されたバージョンを利用している前提で説明しています。
また、node.js は `v6.9.1` を使用しています。詳細は割愛しますが、サンプルアプリケーションを起動したい場合には、任意の手段によって node.js のインストールをして下さい。
サンプルアプリケーション概要
アドベントカレンダー用の記事ということもありますので、今回は Qiita API v2 を例として使用したいと思います。
機能としては、 Qiita API から記事の情報を取得したり投稿したりできるシンプルなアプリケーションです。本番環境では Qiita API へのリクエストを行う一方、開発環境では、APIモックサーバに対してリクエストを行い、モックの値を返してくるという構成で作ってみます。
💡 モックサーバの API仕様について、Qiita API から引用させていただきました。また、リクエスト・レスポンスに利用するモックデータについては、扱いやすいよう加工して利用しています。
Qiita API v2: http://qiita.com/api/v2/docs
サンプルアプリケーション仕様
ビルド・モジュールバンドラツール
サーバアプリケーション構成
APIモックサーバ側
Express
webサーバ側
webpack-dev-server
モックAPI仕様
URL | method | 機能 | 備考 |
---|---|---|---|
/api/v2/items | GET | 投稿の一覧表示 | parameterとして page , per_page , query が設定できる |
/api/v2/items | POST | 新規投稿作成 | parameterとして body , tags , title が設定できる。body 及び title は必須 |
プロジェクト詳細
サーバのプロキシ
一般に、APIサーバとwebサーバを同一のドメイン上で提供する場合には、Nginx などを利用して、各サーバにリバースプロキシを行うと思います。例えば、今回のサンプルアプリケーションの場合は、先に定義した仕様から、本番では以下のように APIサーバ と webサーバとを振り分けることが想定されます。
静的コンテンツ: http://example.com
動的コンテンツ: http://example.com/api/v2
同一ドメインであることの利点はいくつかありますが、APIサーバとwebサーバ間のデータ連携に Ajaxを利用する場合、 同一オリジン でなければ同一オリジンポリシーによって、アクセスが許可されません。
実際のローカル開発においても同様に、APIモックサーバとwebサーバ間では起動ポート(あるいはそれ以外も!)が異なるはずなので、APIモックサーバへのプロキシが行えるとよさそうです。
今回のサンプルアプリケーションでも、APIモックサーバとサンプルアプリケーションのデータ連携には Ajaxを利用しますので、webpack-dev-server のproxy設定を行うことで、本番に近い形式でAPIを叩けるようにしてみたいと思います。
サンプルアプリケーションは http://localhost:8888 で起動するので、以下のようなパスで APIが叩けるとよさそうです。
ここで、APIモックサーバは http://localhost:4200 で起動するため、サンプルアプリケーションでは、http://localhost:8888/api/v2 以下にアクセスをした場合に http://localhost:4200 にプロキシするような設定を行っています。
※ 詳細は割愛しますので、気になる方は webpack-dev-server のプロキシ設定項目をご確認下さい。
サンプルアプリケーションのプロキシ
module.exports = {
...
],
devServer: {
port: 8888,
contentBase: 'dist',
proxy: {
'/api/v2/**': {
target: 'http://localhost:4200'
}
}
}
};
これによって、API のエンドポイントを同一ドメインで実現することができました。試しにwebブラウザのアドレスバーに http://localhost:8888/api/v2/items にアクセスしてみると、モックサーバに定義した(後に APIモックサーバの実装項目でお話します)jsonデータが取得できると思います。
また、target
のホストを名前ベースのバーチャルホストに設定する場合は、合わせて changeOrigin: true
を指定する必要があります。
👮「そうだったのか!」
例えば、http://localhost:8888/api 以下にアクセスした場合に https://status.github.com にプロキシしたい場合には、以下のように設定を変更します。
module.exports = {
...
],
devServer: {
port: 8888,
contentBase: 'dist',
proxy: {
'/api/**': {
target: 'https://status.github.com',
changeOrigin: true
}
}
}
};
当たり前ですが、実際のリバースプロキシサーバの設定に即したプロキシ設定をしていないと、ローカルではうまく動くのに、ビルドしてデプロイしたけれどうまくいかない、ということになってしまいますので注意が必要です。
※ 詳しくは http-proxy-middleware をご確認下さい。
モックサーバの実装
実際に ajax でリクエストを行うコードを確認する前に、APIモックサーバの実装を確認しておきましょう。
※ こちらも詳細は割愛しますので、気になる方は Express 公式のAPIドキュメントをご確認下さい。
一覧取得エンドポイント実装
まずはエンドポイント /api/v2/items
に対してメソッド GET
でリクエストが行われた場合について確認してみます。
...
app.get('/api/v2/items', (req, res) => {
const query = req.query;
const page = query.page;
const perPage = query.per_page;
const searchQuery = query.query;
fs.readFile('mock-server/data/items.json', (err, data) => {
if (err) throw err;
if (data) {
setTimeout(() => {
try {
let parsedData = JSON.parse(data);
if (perPage) {
parsedData = parsedData.slice(0, perPage);
}
res.status(200).send(parsedData);
} catch (ex) {
res.status(400).send(clientErrorObject);
}
}, 2000);
} else {
res.status(500).send(serverErrorObject);
}
});
});
...
順を追って正常系の挙動を確認してみます。
- Ajax のリクエスト時にクエリとして渡された値を req.query で取得
- mock-server/data/items.json を読み込んで、jsonをパースした後 JavaScript のオブジェクトに変換
- クエリとして渡ってきた perPage の値があれば、パースした itemsの配列から perPage 個数分絞り込み
- 値をレスポンスとして送信
mock-server/data/items.json は、あらかじめ格納しておいたモックデータの一つで、投稿のモックデータを配列として定義しています。モックサーバでは、DB等から取得してくる代わりに、固定で値を返すようにしています。
items.json について、パースした情報をそのまま返却してもよいのですが、ここではせっかく渡ってきたクエリがあるので、記事のモックデータを返す際、クエリの値により返却する個数変化を確認できるようにしています。
最終的に、jsonデータとして画面側にレスポンスデータを送信します。
新規投稿エンドポイント実装
次にエンドポイント /api/v2/items
に対してメソッド POST
でリクエストが行われた場合について確認してみます。
...
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
...
app.post('/api/v2/items', (req, res) => {
const body = req.body;
if (!body.title || !body.body) {
res.status(400).send(clientErrorObject);
return;
}
fs.readFile('mock-server/data/item.json', (err, data) => {
if (err) throw err;
if (data) {
setTimeout(() => {
try {
const item = JSON.parse(data);
item.body = body.body;
item.title = body.title;
if (body.tags) {
item.tags = body.tags.split(' ');
}
res.status(201).send(item);
} catch (ex) {
res.status(400).send(clientErrorObject);
}
}, 2000);
} else {
res.status(500).send(serverErrorObject);
}
});
});
こちらも順を追って正常系の挙動を確認してみます。
-
application/json
タイプ,application/x-www-form-urlencoded
タイプの postデータをパースできるようにしておく(body-parser
利用) - リクエストデータ内を確認して、記事のタイトル、記事の本文のどちらかが存在しなければ、エラーを返す
- mock-server/data/item.json を読み込んで、jsonをパースした後 JavaScript のオブジェクトに変換
- 変換したオブジェクト item に対して、取得した記事タイトル、記事本文、存在すればタグを上書き
- 値をレスポンスとして送信
サンプルアプリケーションの Ajax 実装
最後にサンプルアプリケーション側で Ajax を行って、レスポンスデータを表示するところを確認してみます。
一覧取得実装
まずは一覧取得を行っている箇所です。ボタンを押したらデータを取得して表示します。
$.ajax()
に渡すパラメータのうち、APIモックサーバでクエリとして利用したのが記憶に新しい per_page
はここで定義されています。
...
const getItemsButton = $('#get_items_button');
const resultElement = $('.result span');
getItemsButton.on('click', () => {
const url = '/api/v2/items';
resultElement.text('loading...');
$.ajax({
url: url,
type: 'GET',
data: {
page: 1,
per_page: 2,
query: 'sakura'
}
})
.then(data => {
console.log('data: ', data);
resultElement.text(JSON.stringify(data));
}).fail(xhr => {
console.log(xhr.responseJSON);
resultElement.text(JSON.stringify(xhr.responseJSON));
});
});
...
ブラウザで http://localhost:8888 にアクセスして、ページ上部の緑色のボタン「投稿一覧取得」をクリックしてみると、ページ下部にレスポンスデータが表示されると思います。
また、ブラウザのデバッガを開くと、コンソールに取得したデータが表示されていると思います。もちろんネットワークからレスポンスデータを確認してもよいです。
新規投稿実装
次に、新規投稿を行っている箇所の実装です。フォームに入力された値をシリアライズしてデータとして渡しているだけのシンプルな実装です。
...
const getItemsButton = $('#get_items_button');
...
const postItemForm = $('#post_item_form');
postItemForm.on('submit', event => {
event.preventDefault();
const url = '/api/v2/items';
resultElement.text('loading...');
$.ajax({
url: url,
type: 'POST',
data: postItemForm.serialize()
})
.then(data => {
console.log('data', data);
resultElement.text(JSON.stringify(data));
}).fail(xhr => {
console.log(xhr.responseJSON);
resultElement.text(JSON.stringify(xhr.responseJSON));
});
});
...
こちらもブラウザで http://localhost:8888 にアクセスして、フォームの title, tags, body にそれぞれ値を入力した後、上部の緑色のボタン「投稿する」を押してみます。ページ下部にレスポンスデータが表示されると思います。
GET
と異なる点としては、フォームに入力した値に応じてレスポンスデータが変化するところです。これは先程、モックサーバ側で、リクエストで渡ってきた値に応じて モックjsonデータを書き換えて、レスポンスとして送信するという実装を行っているためです。
また、必須項目 title もしくは body を空のまま「投稿する」を押してみます。すると、事前に定義されたエラーメッセージが返ってくることがわかります。
☕️ APIモックサーバ側の実装では、エラー時のステータスコードは `400` と `500` の2種類しか定義しませんでした。本来ならば、エラーに応じてより適切なステータスコードが返却される可能性があります。
フロントエンド側で、ステータスコードの値に応じて処理をわけたい場合には、モックサーバ側でより細かにエラーハンドリングを行い、レスポンスを行う必要があります。
プロキシを使わない別ドメインへのリクエスト
ここまでで大まかですが、サンプルアプリケーションから、プロキシ機能を利用して APIモックサーバと接続してデータ連携の挙動を確認することができました。ですが、実際のプロジェクトでは、同一ドメイン上に存在しない APIのパスを指定しないといけないことも珍しくありません。
これまでみてきたように、プロキシによって同一オリジンとしてリクエストを行うという方法を選択できればよいのですが、外部サービスの公開APIをリクエストするのに、毎回同一ドメインとしてプロキシ設定を追加するのは少し手間です。
そこで次に、CORSの仕組みを利用して、クロスオリジンでアクセスする方法について確認します。
APIモックサーバへのリクエスト
まずは、ローカルに構築しているAPIモックサーバに対して、クロスオリジンでリクエストを行ってみます。
これまで Ajax のリクエストURLに相対パスで渡していた部分を、絶対パスを渡してあげるように変更します。これでプロキシを経由せず、直に http://localhost:4200 にクロスドメインでリクエストを行うようになりました。
...
getItemsButton.on('click', () => {
const url = 'http://localhost:4200/api/v2/items';
...
このままではクロスオリジン制限によりアクセスに失敗してしまいますので、APIモックサーバ側で CORS対応が必要です。以下の通り Access-Control-Allow-Origin ヘッダにリクエスト元の http://localhost:8888 を許可するよう指定します。
...
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:8888');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
...
このようにして、APIサーバを一度再起動して、記事一覧取得をしてみると、無事クロスオリジンでもリソースにアクセスできるようになりました。
Qiita APIサーバへのリクエスト
次に、Qiita の API へのクロスオリジンでリクエストをおこないたいと思います。
...
getItemsButton.on('click', () => {
const url = 'http://qiita.com/api/v2/items';
...
運がよいことに、Qiita は CORS対応している(レスポンスヘッダの情報をみると CORS に関する情報が含まれている)ので、外部ドメインからのアクセスすることができます。こちらも記事一覧取得をしてみると、無事リクエストに成功しました。
※ なお、POST に関しては 401 (Unauthorized)
が返ってきます。当然ですが、認証していない状態では投稿のしようがありません。今回のサンプルでは実装を割愛しました。
環境変数による挙動変更処理
最後に、アプリケーションの起動時にパラメータとして渡す値によって、APIの向き先を変更してみます。
以下のようにしておくと、ビルド時にパラーメータとして渡した値を、webpack で作成しているアプリケーション内で利用できるようになります。
webpack の設定ファイルに以下のように追記します。このように設定すると、webpack のプロジェクト内の process.env.NODE_ENV
がビルド時の値に置きかわります。
const webpack = require('webpack');
...
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
})
],
...
# 起動時に staging という文字列を渡す例
NODE_ENV=staging npm run app
webpack経由で取得した process.env.NODE_ENV
という変数から値を取得して、値に応じて APIサーバの向き先を変更しています。
const NODE_NEV = process.env.NODE_ENV;
export const API_SERVER_HOST =
(NODE_NEV !== 'production' && NODE_NEV !== 'staging')
? 'http://localhost:4200'
: 'http://qiita.com'
...
import { API_SERVER_HOST } from './environments';
...
getItemsButton.on('click', () => {
const url = `${API_SERVER_HOST}/api/v2/items`;
...
※ 詳細は dependency injection項を ご確認下さい。
モックサーバ駆動開発
今回はさくらのフロントエンド開発をモックサーバとの連携という観点で書いてみました。以下、モックサーバを使用した開発におけるメリット・デメリット、課題などを述べて終わりたいと思います。
モックの粒度
サンプルアプリケーションについては、説明のために、モックデータの処理に関する粒度が荒かったり、逆に局所的に細かすぎたりするところがありました。また、実際にはステータスコードやエラーメッセージも少し丁寧にハンドリングしたいこともあるかと思います。
どこまでの粒度でモックサーバを実装するかは、プロジェクトの種類や規模によると思います。ただ、一つの観点として、モックサーバの作成を頑張りすぎて、実際のバックエンドAPIサーバの実装と大きくかぶってしまうところまでは作り込む必要はないと思います。
モックAPIの実装に時間を取られすぎて、本来必要なタスクへの時間割当を誤っては目も当てられませんね!
APIモックサーバとテスト
むしろ、細かな取得データに関する確認作業は、テストを書くなどすれば不要な場合も多いでしょう。モックサーバは、あるエンドポイントに対してリクエストを行ったら、期待した値が固定で返ってくるくらいのことが確認できる程度から始めるのがよいのではないでしょうか。
これは個人的な感想ですが、モックサーバを作り始めてから随分機能の実装漏れが減ったと感じています。
まとめ
🎅「ながい」
var c=[];document.getElementById('article-body-wrapper').querySelectorAll('em').forEach(function(e){ c.push(e.textContent);});c.reverse().join('');