CORSを解決してローカル環境で2つのサービスを動かしたい

ブラウザのWebページから、別サーバのWebAPIを呼び出すという動作検証を両方ローカル環境でやりたい、というのが今回の趣旨です。
次のような2つのサービスを用意して、ローカル環境で動作させます。

サービス URL
Webページ localhost:5000で起動。アクセスするとWebAPIを呼び出す。
WebAPI localhost:8000で起動

初回コード(これは失敗します)

まず最初に作ったのは次のようなコードです。

ブラウザサービス側(localhost:5000)

window.onload = function() {
        const data = { name: 'example' };
        fetch('http://localhost:8000')
            .then(response => response.json())
            .then(data => {console.log(data);})
            .catch(function(error) {
                console.log("error:", error);
            });
}

WebAPI側(localhost:8000)

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, User{
            Name: "hoge",
        })
    })
    r.Run(":8000")
}

これを両方とも起動して、ブラウザサービスにアクセスするとWebAPIが呼び出されるのですが、CORSの問題で次の通りアクセスエラーが起きます。

Access to fetch at 'http://localhost:8000/' from origin 'http://localhost:5000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

最終コード(これで解決しました)

色々調べた結果、WebAPI側で「Access-Control-Allow-Origin」レスポンスヘッダを付ければいいということが大枠で分かりました。 goで実装しているWebAPIはginを使っているのですが、CORSを解決してくれるパッケージがあるようなのでそれを使って解決しました。
こちらのサイトを参考にしています。
http://psychedelicnekopunch.com/archives/820

WebAPI側(localhost:8000)を改造

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    // CORS 対応を追加
    config := cors.DefaultConfig()
    config.AllowOrigins = []string{"http://localhost:5000"}
    r.Use(cors.New(config))

    r.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, User{
            Name: "hoge",
        })
    })
    r.Run(":8000")
}

やってみたけど駄目だったこと

この先は解決に至るまでに色々試行錯誤していた内容を載せておきます。

最初、fetchを使ったCORSの解決方法を探していて、こちらのサイトmodeを指定できることを知りました。 https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch

リクエストヘッダのmodeは"no-cors", "cors", "same-origin"を指定できるのですが、"no-cors"を使えばとりあえず動くかな?と思い、設定してみました。

ブラウザサービス側(localhost:5000)を改造

window.onload = function() {
        const data = { name: 'example' };
        fetch('http://localhost:8000', {
            method: 'GET',
            mode: "no-cors",
            headers: {
              'Content-Type': 'application/json'},
            })
            .then(response => response.json())
            .then(data => {console.log(data);})
            .catch(function(error) {
                console.log("error:", error);
            });
}

この方法は、WebAPIは呼べるようになるのですが、受け取ったデータにjsonデータが含まれなくなります。 結局これだとレスポンスデータを受け取れなくなるようです。 さらに上のリンクのサイトにも次のように注意書きがありました。

なお、 mode: "no-cors" はリクエスト中の限られた数のヘッダーにしか許可されていません。

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type のうち、値が application/x-www-form-urlencoded, multipart/form-data, text/plain のいずれかのもの

M5StickCでHTTP GETする

こちらの記事をみて自分もやってみました。
https://slanew.com/news/668
元記事はC言語で実装していますが、私は今回UIflowで作りました。

完成品

bitflyerのWebAPIからビットコインの現在の価格を取得して表示します。

f:id:bluemon0919:20210214130824j:plain
M5StickC BitcoinValue

UIflowで実装する

UIFlowを開いてBlocklyで組んでいきます。
M5ボタン(Aボタン)を押したら表示を開始して、5秒間隔で更新します。

f:id:bluemon0919:20210214134507p:plain
UIflow

肝になるのはHTTP GETの部分です。
Advance > Http > Http Requestを選択します。

Http Requestで受け取ったデータはGet Dataで受け取れるため、これをjsonで読み込んで"best_bid"というキーのデータを表示します。

もやもやポイント

M5StickCのパワーが足りないのか分かりませんが、M5ボタン押してもhttp requestで取得した値が表示されないことがあります。
一度動けばその後問題はないのですが、結構頻繁に発生するのでなんとか解消したいところです。

Go Modulesでローカルパッケージをインポートする

まさに欲しい情報が書いてあるページを見つけました。

zenn.dev

GitHubにアップするまではexampleというモジュール名を仮でつけておくことが多いのですが、
その場合だと次のように書けばapiパッケージをインポートできます。

package main

import (
    "example/api"
)

再帰呼び出しになるかどうか

次のエラーで少しハマったので調べました。
Uncaught (in promise) RangeError: Maximum call stack size exceeded

どうやら再帰呼び出しによるスタックオーバーフローのようです。
実行していたコードは以下の通り。

let drawPattern = true;
function messageDraw(player_name) {
  document.getElementById("log_name").textContent = "Name : " + player_name
  if (drawPattern) {
    document.getElementById("message").textContent = "( ・ω・)";
  } else {
    document.getElementById("message").textContent = " (・ω・ )";
  }
  drawPattern = !drawPattern;
  setTimeout(messageDraw(player_name), 500);
}

なるほど、messageDrawの呼び出しが問題のようです。
このコードを次のように修正するとスタックオーバーフローは発生しなくなりました。

let drawPattern = true;
let player_name = "hoge"
function messageDraw() {
  document.getElementById("log_name").textContent = "Name : " + player_name
  if (drawPattern) {
    document.getElementById("message").textContent = "( ・ω・)";
  } else {
    document.getElementById("message").textContent = " (・ω・ )";
  }
  drawPattern = !drawPattern;
  setTimeout(messageDraw, 500);
}

ふむふむ、なぜ?
関数を引数に与える形式で記述すると再帰呼び出しでは無くなるようですが、理屈が分からない。 おそらくスコープ内の変数が無いので、setTimeout()タイムアウトmessageDraw()を呼び出した時点で前のスタックが解消されるのだと想像します。

Javascriptの同期処理についてメモっておく

サーバからユーザ情報を取得して表示を更新する処理を作るに当たって、httpの完了を待つ処理が必要でした。
#3 クライアントでプレイヤー情報を参照する · bluemon0919/lobby@8f4b0f2 · GitHub

こちらの記事を参考に作りました。

var u1_name;  // これを更新する
var u2_name;

function getUserInformation() {
    const p = new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', "/players/" + u2, true);
        xhr.responseType = 'json';
        xhr.onload = function() {
            var res_data = this.response;
            u1_name = res_data[u1];
            u2_name = res_data[u2];
            resolve();  // 処理の終わりを通知する
        };
        xhr.send();
    });
    return p;
}

function init() {
    ...
    getUserInformation().then(() => update());  // getUserInformationが終わったらupdateを行う
}

上の例では使っていませんが、 resolveメソッドに結果を渡し、それをthenメソッドの中で受け取ることができます。
次の例では文字列"done"を渡してconsole.logで出力します。

function getUserInformation() {
    const p = new Promise((resolve, reject) => {
        ...
        resolve("done");
    }
}

function init() {
    ...
    getUserInformation().then((result) => {
        console.log(result);
        update();
    });  // getUserInformationが終わったらupdateを行う
}

dockerの基本コマンド

はじめに

dockerを勉強したかったので、tutorialから手をつけてみました。

#LearnDocker | Docker

Dockerイメージの理解を目指すチュートリアル - Qiita

出てきたコマンドを忘れそうだったので、 備忘録的にdockerの基本コマンドをまとめてみた記事です。

docker pull

docker hubからイメージやリポジトリを取得するためのコマンド。
name spaceやtagは省略でき、name spaceを省略した場合はdocker公式のname space=libraryになり、 tagを省略した場合はtag=latestになる。

docker pull {<name spece>"/"}<repository>{":"<tag>}

$ docker pull hello-world
or
$ docker pull library/hello-world:latest

docker images

docker イメージ一覧を表示する。

$ docker images

docker inspect

dockerイメージの詳細を表示するコマンド。
name spaceやtagは省略でき、name spaceを省略した場合はdocker公式のname space=libraryになり、 tagを省略した場合はtag=latestになる。

docker inspect {<name spece>}<repository>{":"<tag>}

$ docker inspect hello-world:latest

docker rmi

dockerイメージを削除するコマンド。

docker rmi <repository>

$ docker rmi hello-world

docker run

dockerイメージからdockerコンテナを起動するコマンド。

docker run <ContainerID>|<ContainerName>{":"<tag>}

$ docker run hello-world

docker ps

dockerコンテナの状態を確認するコマンド。
option指定はこちらを参照。

docker ps [option]

$ docker ps -a

docker rm

dockerコンテナを削除するコマンド。
option指定はこちらを参照。

docker rm [option] <Container ID>|<Container Name>

$ docker rm hello-world

docker build

dockerイメージをビルドするコマンド。 パスに指定したディレクトリにあるDockerfileとコンテキストを利用して新しいDockerイメージを構築する。 コンテキストはDockerイメージ構築に必要なファイル全般のことを指す。
-tContainerName:tag形式指定のオプションで、ビルド時にタグを追加することもできる。

docker build -t <ContainerName>{":"<tag>}

$ docker build -t myhello .
or
$ docker build -t myhello:v2 .

終わり

もう少し勉強したらdockerのライフサイクルをまとめてみようと思います。

docker for macでttyにアクセスできない

macの場合dockerがVM上で動作しているので、/var/lib/docker/に直接アクセスできない。

$ docker inspect hello-world:latest
...
"GraphDriver": {
            "Data": {
                "MergedDir": "/var/lib/docker/overlay2/a561cd4994ad3cc18999f56d565a9c2d703f3bcfb17bb30374874dd6da80e88b/merged",
                "UpperDir": "/var/lib/docker/overlay2/a561cd4994ad3cc18999f56d565a9c2d703f3bcfb17bb30374874dd6da80e88b/diff",
                "WorkDir": "/var/lib/docker/overlay2/a561cd4994ad3cc18999f56d565a9c2d703f3bcfb17bb30374874dd6da80e88b/work"
            },
            "Name": "overlay2"
},

次のコマンドでttyにアクセスできるらしいのだが、ファイルが無いというエラーが出てしまった。

$ screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty
...
No such file or directory

確かにttyというファイルは無い。バージョンによる違いだろうか。

$ ls -l ~/Library/Containers/com.docker.docker/Data/vms/0
total 144
srwxr-xr-x  1 kota  staff      0  1  4 10:00 00000002.000005f4
srwxr-xr-x  1 kota  staff      0  1  4 10:00 00000002.00001003
srwxr-xr-x  1 kota  staff      0  1  4 10:00 00000003.000005f5
srwxr-xr-x  1 kota  staff      0  1  4 10:00 00000003.00000948
srwxr-xr-x  1 kota  staff      0  1  4 10:00 connect
-rw-r--r--  1 kota  staff  65536  1  4 10:00 console-ring
drwxr-xr-x@ 3 kota  staff     96  1  4 10:00 data
lrwxr-xr-x  1 kota  staff     17  1  4 10:00 guest.000005f5 -> 00000003.000005f5
lrwxr-xr-x  1 kota  staff     17  1  4 10:00 guest.00000948 -> 00000003.00000948
-rw-r--r--  1 kota  staff   2375  1  4 10:00 hyperkit.json
-rw-r--r--  1 kota  staff      4  1  4 10:00 hyperkit.pid
drwxr-xr-x  2 kota  staff     64  1  4 10:00 log

使っているバージョンは次の通り。何か情報あったら教えてください。

$ docker version
Client: Docker Engine - Community
 Cloud integration: 1.0.4
 Version:           20.10.0
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        7287ab3
 Built:             Tue Dec  8 18:55:43 2020
 OS/Arch:           darwin/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.0
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       eeddea2
  Built:            Tue Dec  8 18:58:04 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.4.3
  GitCommit:        269548fa27e0089a8b8278fc4fc781d7f65a939b
 runc:
  Version:          1.0.0-rc92
  GitCommit:        ff819c7e9184c13b7c2607fe6c30ae19403a7aff
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0