概要
このエントリは、「Enterprise "hello, world" 2018 Advent Calendar 2018」の12/19向けのものです。このAdvent Calendarでは、複数個のエントリにまたがる話の流れも鑑みつつ、なるべく1エントリで1つのトピックをカバーできるようにする予定です。
このエントリで記載するトピックは、開発時にwebpack-dev-serverを使ってフロントからAPIサーバを呼ぶこと(並びに既存のテストケースの修正)です。ここまでの構成だと、ブラウザのCORS対策の制限に引っかかるため、開発時に便利になるように一工夫します。(フロント界隈の方には常識だと思いますがAdvent Calendarの流れとしてここで取り上げます。)
前提
おことわり
- このEnterpfise "hello, world"シリーズは、ネタのためのエントリです。実環境でそのまま利用ことを目的とはしていません。
- 動かしやすさを優先してセキュリティを意図的に低くする設定など入れてありますのでご注意ください。
想定読者
「Enterprise "hello, world" 2018」的なネタとしては、下記のような状況を想定しています。
フロント側からAPIを呼ぶように改変するにあたり、楽に開発できるようにしたい。ブラウザのCORS対策によってAPIが呼べない状況を避けたい。
開発時にフロント側からAPIを呼びやすくする
ここまでの構成で起こること
Advent Calendarでは、ここまでの流れでフロントとAPIサーバを別々に作ってきましたが、その2つを別々に起動してリクエストを飛ばそうとするとき、
Cross-Originのリクエストとなってしまい、ブラウザがリクエストを止めます(下図)。
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8080/api/greeting. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).[Learn More]
webpack-dev-serverでAPIへのリクエストをプロキシ
この状況を避けるため、下記のように、webpack-dev-serverを使い、フロント側のファイルの配信とAPIサーバ側へのリバースプロキシの2つの役割を持たせて、ブラウザから見たときの接続先を1つにします。
[Browser] --> [WebPack dev server] --> "/" --> local file(html+js)
--> "/api" --> to API Server
手順
webpack-dev-serverをインストールします。
$ npm i webpack-dev-server --save-dev
wepack.config.jsを作ります。proxyを指定している部分で、APIサーバへの転送を指定しています。
出力するファイルの場所は、webpack-dev-serverが扱いやすいよう、今までと場所を変えています。
module.exports = {
devServer: {
host: "localhost",
port: 3000,
disableHostCheck: true,
proxy: {
"/api": {
target: "http://localhost:8080/"
}
},
historyApiFallback: {
index: 'index.html'
}
},
output: {
publicPath: "/",
filename: "bundle.js"
}
};
package.jsonに、webpack-dev-serverの起動用のスクリプト(下記で"start:dev")を追加します。
"scripts": {
"build": "webpack src/index.js --output bin/app.js -p",
"start": "webpack src/index.js --output bin/app.js -d --watch",
"start:dev": "webpack-dev-server --mode=development --host=localhost --watch-content-base --open-page index.html",
"test": "ospec"
},
"$ npm run start:dev"でwebpack-dev-serverが起動し、"http://localhost:3000/" でリクエストが受けられる状態になり、「/api」以下へのリクエストは、APIサーバ側に転送されます。これでこのエントリの目的が達成できました。
テストの修正
話が前後しますが、フロント側からバック側に、メッセージ取得のためのAPIを呼ぶ機能を追加しました。あわせて、テストのしやすさも考え、JSのオブジェクトとして動いていたものJSのクラスに変えたりなどの変更を加えています。
メイン
index.js
"use strict";
const m = require("mithril");
const GreetingMessage = require("./models/greeting-message");
const greetingMessage = new GreetingMessage();
greetingMessage.loadMessage();
setTimeout(function() {
const HelloWorldComponent = require("./views/helloworld-component");
const helloWorldComponent = new HelloWorldComponent(greetingMessage);
m.mount(document.body, helloWorldComponent);
}, 1000);
helloworld-component.js
メッセージの取得は他に任せることにし、このコンポーネントは表示するだけです。
const m = require("mithril");
class HelloWorldComponent {
constructor(greetingMessage) {
console.log("HelloWorldComponent .ctor")
this.greetingMessage = greetingMessage;
}
view() {
return m("div", m("p", this.greetingMessage.getText()));
}
};
module.exports = HelloWorldComponent;
greeting-message.js
うーん、なんかもうちょっといい書き方がある気がするのですが、メッセージの取得と保持の役割を持たせてみました。
const m = require("mithril");
class GreetingMessage {
constructor() {
this.text = "initial";
}
loadMessage() {
console.log("begin loadMessage()");
const self = this;
m.request({
method: "GET",
url: "/api/greeting",
withCredentials: true
}).then(function(res) {
console.log("returned:" + res)
self.setText(res.message);
});
}
getText() {
return this.text;
}
setText(text) {
console.log("set text to " + text)
this.text = text;
}
};
module.exports = GreetingMessage;
テスト
init.js
mithrilとospecをブラウザ無しで動作させるため、globalのwindowを入れ替えます。
const mock = require("mithril/test-utils/browserMock")();
global.window = mock;
global.document = mock.document;
helloworld-component.js
メッセージの取得部分は外部クラスにおまかせし、表示のテストだけをします。
const o = require("mithril/ospec/ospec");
// mock
const greetingMessage = {
getText: () => {
return "hello, world";
}
};
var HelloWorldComponent = require("../../src/views/helloworld-component");
helloWorldComponent = new HelloWorldComponent(greetingMessage);
o.spec("HelloWorldComponent", function() {
o("returns a div", function() {
var vnode = helloWorldComponent.view();
o(vnode.tag).equals("div");
o(vnode.children.length).equals(1);
o(vnode.children[0].tag).equals("p");
o(vnode.children[0].text).equals("hello, world");
});
});
greeting-message.js
モックのwindowオブジェクトに対し、GETメッセージ発行時の処理を指定し、テストを走らせます。setTimeoutしているところはもうちょっとマシな書き方があるような気がするのですが、時間オーバーによりこの状態で貼ります。
const o = require("mithril/ospec/ospec");
const callAsync = require("mithril/test-utils/callAsync");
const GreetingMessage = require("../../src/models/greeting-message");
const greetingMessage = new GreetingMessage();
o.spec("GreetingMessage", function() {
o.before(function(done){
console.log("running before()");
window.$defineRoutes({
'GET /api/greeting': function(req) {
console.log("GET called.");
return {
status: 200,
responseText: JSON.stringify({
message: 'hello, world'
})
}
}
});
done();
});
o("returns a message", function(done, timeout) {
timeout(1000);
greetingMessage.loadMessage();
// API call should be done in 100ms
setTimeout(() => {
o(greetingMessage.getText()).equals("hello, world");
}, 100);
done();
});
});
テスト実行
$ npm test
> frontend-mithril@0.0.1 test /home/hiroki/work/frontend-mithril
> ospec
HelloWorldComponent .ctor
running before()
begin loadMessage()
GET called.
6 assertions completed in 21ms, of which 0 failed
returned:[object Object]
set text to hello, world
テストが成功しました。
まとめ
このエントリでは、「Enterprise "hello, world" 2018 Advent Calendar 2018」(EHW2018)の19日目として、開発時にwebpack-dev-serverを使ってフロントからAPIサーバを呼ぶこと(並びに既存のテストケースの修正)をトピックとして取り上げました。
本エントリのソースコードは、https://github.com/hrkt/frontend-mithril/releases/tag/0.0.3にあげてあります。
EHW2018のネタとしては、このあと、デプロイの準備をすすめることを考えています。