【React】SNSでの議論をマシにするサービス開発 ~第2弾(フロント編)~【1/31記事目】

こんにちは。本記事は、1ヶ月間ブログ連投チャレンジの1日目の記事です。

今回は「SNS上の議論をマシにできないか」と思い、開発し始めたサービスのフロントエンド編で、 ↓ の記事の続きになります。

ume-boshi.hatenablog.jp

ume-boshi.hatenablog.jp

とはいっても私にweb系の専門知識はなく、淡々と開発していただけなので、あまり技術的な話はできなさそうです。


現在の動作イメージ

まずは最終的な動作イメージを先に載せておきます。 youtu.be

画面遷移

動画でも見られますが、画像でも説明しておきます。

まず、ホーム画面を作成しました。とはいっても、カード作成に向かうボタンしか機能はしていません。。。 そのうち #ハッシュタグ一覧 や、タグごとに記入項目を調節できるような機能も実装するかもしれません。

f:id:ume-boshi:20210501062946j:plain
ホーム画面。まだカード作成面しか実装していない

ホーム画面で「カードを作成」ボタンをクリックすると、カード生成画面に移行します。
この画面では、初期状態で{text入力 + プルダウンメニューによる入力エリア}と、「カード生成」ボタンで構成されています。入力エリアに情報をある程度入力すると、カード生成できるようになります。このときの必須入力項目は、「ハッシュタグ」「立場」「背景1」の3項目になります。

f:id:ume-boshi:20210501062726j:plain
カード作成画面の初期状態

「カード生成」を行うと、サーバ側で生成された画像が下側に表示されます。この内容を見て、入力内容に問題がないかを判断してもらい、ローカルに画像をダウンロードしてもらいます。一度カードを生成した後でも、入力内容を更新して画像出力し直すことが可能なので、自分の望むカードが比較的簡単に作れます!

f:id:ume-boshi:20210501062730j:plain
カード生成後の画面。下のボタンをクリックするとDLできる。

最終的にDLしたカードをtwitterに貼り付けて、本文中で意見を述べるという感じで使用する想定です。

f:id:ume-boshi:20210501064136j:plain
現状ではSNS上に手動でupする必要がある。


実装について

前回はGo言語を用いて、バックエンド側を実装しました。具体的にはPOSTで送信したjson形式のクエリに基づいて、画像を生成してresponseするような機能を持たせました。

今回はそのクエリを送るためのフロントエンド実装をメインに行なっています。

動作環境

詳細な実装内容は下記のようになっています。

バックエンド側
- 動作場所:Heroku
- 実装言語:go言語(1.15.5)
- 変更箇所:port番号(記事での説明は省略します)
※Herokuにdeployしただけです。

フロントエンド側
- OS:Windows10
- 実装言語:javascript
- フレームワーク:react, gatsby
- 動作場所:localhost
- 動作検証ブラウザ:Chrome, Vivaldi
※reactやgatsbyを使用していますが、その本領は全く活かせていない気がします。今度しっかり勉強したい。。。

現在、システムは下図のような構成で動作・通信しています。

f:id:ume-boshi:20210501062506p:plain
現状のシステム構成図

REST APIサーバからの画像取得について

サーバから画像を取得する方法について、私の環境で正常に動作したものを掲載します。

//★1
let query = {
    Hashtag: this.state.hashtag,
    ︙

};

fetch("https://hogehoge/createImage", {
    method: "POST",
    headers: {
//★2
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: JSON.stringify(query)  //★1
})
.then(response => {
    if (!response.ok) {
        throw new Error('Network response was not ok');
    }
//★3
    return response.blob();  // returnがないと正常に動作しない
    })
.then(myBlob => {
//★4
    const path = window.webkitURL.createObjectURL(myBlob)
    this.setState({ imageSrc: path, showFlag: true });
    console.log(myBlob);
})
.catch(error => {
    console.error('There has been a problem with your fetch operation:', error);
});
︙

render() {
    return <>
     ︙

      <img
          src={this.state.imageSrc}   {/* ★4 */}
          alt="created twitter card"
          style={{ width: 720, display: `block`, marginBottom: `1.45rem`, marginLeft: `auto`, marginRight: `auto` }}
      />
      ︙

★1:サーバに送る用のクエリを作成します。これは入力エリアの内容を、this.state.hogehogeと呼んで読み込んでいます。
そしてそれをJSON.stringify(query)することで、json形式のデータで送信できるようにしました 。


★2:通常、json形式のデータを送信する場合、"application/json"を使用するはずです。しかし"application/json"でPOST送信する場合に、謎の現象が生じます。というのも、POST requestが送られる前に、OPTION requestなるものが送信されてしまいます。
これをpreflight requestと呼ぶらしく、simple requestsではない場合に送られるそうです。つまりapplication/jsonは高度な送信手法とみなされ、追加情報を先に送ってしまうらしい。そんなもん素人が知るかよ。。。

var.blog.jp

また、CORSの問題についてもこの書き方でおよそ解決した覚えがあります。"application/json"でrequestした場合、fetchの際にブラウザでCORSエラーが生じていました。CORSエラーが生じる場合、" 'mode' : 'no-cors' "をすると表面上は解決するのですが、no-corsモードでrequestを送信している場合は"opaque"という無意味なresponseが送られてきました。

この"opaque"なcontent-typeのレスポンスは、OPTION requestが送られる際にフロントで得られるのですが、システム的には想定外のresponseでした。それに対し、"application/x-www-form-urlencoded"を用いると、OPTION requestが送信されないためCORSエラーも生じないで想定通りの動作を実現できました。

私はこのエラーが他の部分が原因であり、画像が読み込めていないと勘違いしていたため、このopaqueなresponseにはかなり悩まされました。 とにかく、CORSに関するエラーが生じた際は、適当に"no-cors"で乗り切るよりも、根本的な問題を解決することが吉な気がします。


★3:fetchで画像を受け取ってURLを生成する方法は、ネットに結構上がっているのですが、そのどれもが私の環境では想定動作をしませんでした。fetchで画像を得るにはPromise関数というJSの構文を使っているらしいのですが、その".then"の値のやり取り方法が理解できていませんでした。
Promise関数では、returnを使う場合と使わない場合があり、使う場合はその返り値を次の.thenの開始地点とできるみたいです。つまり★3の箇所では、responseの内容を一度blob形式(ファイル管理によく使われるらしい)に変換し、それを次の.thenで使用しています。

ここでreturnしなかった場合、myBlobの内容はundefineな状態になり画像をうまく読み込めません。 returnをした場合は、正常にblobが読み込めるため、ようやくcreateObjectURLができるようになります。


★4:Blob形式のデータを基に、そのデータにアクセスするためのURLを生成しています。この際"window.URL.createObjectURL"という関数も広く使用されているらしいのですが、これはchromeでは正常に動作しない場合があるそうです。

ここで生成できたURLは、this.setState({ })で保存して、それをrender()内のタグのsrcに突っ込んであげることで表示できました。


おわりに

このサービスに関する次の記事は、「セキュリティ編」になります。そのときには準完成形としては紹介するかも。投稿は来月かな?

今回の開発を通して、web系に関する非同期処理であったりhttpの通信仕様に至るまで全く理解できていないことを痛感しました。意外に日本語の情報は少なく頼りにならず、海外サイトにばかり頼っていました。正直、多くのwebエンジニアが詰まること無く進められていた(記事数が多くなかったので)と考えると、今後自分がまともにやっていけるか不安でしかないです。

まあ、今後も少しずつ、着実に勉強をしていきたいなと思いました。 では、また明日もよろしくおねがいします~