フロントからWebスクレイピング ( react, express 連携 )

フロントからWebスクレイピンを行おうとするとCORSのエラーでだいたいはまる．．．
相手側の設定次第では回避できる時もあるがフロントのみだと解決できない．．．
そこで express で簡易にバックエンドのサーバ作って対応してみる
スクレイピングには jsdom 使ってます
サンプルでは yahoo news の画像を適当にとってきてます

フロントのみで CORS のエラー解消する方法あったら教えてください！！

# 実施環境
- Windows11
- node (v18.17.1) ※インストール済
- react@18.2.0
- express@4.18.2

# 手順

## react プロジェクト作成・移動 (create-react-app)
create-react-app を使ってプロジェクトを作成
プロジェクト名は任意でOK！
- プロジェクト作成
```powershell
npx create-react-app project-name
```
- プロジェクト移動
```powershell
cd project-name
```

## express インストール（バックエンドサーバ）
express のインストールの他に開発などを楽にするやつもインストール
・nodemon : バックエンド(express)起動時自動反映するため
・npm-run-all : フロント(react), バックエンド(express) を同時起動するため
・pino : express のログ出力するため

- express
```powershell
npm i express
```
- 起動時反映・同時実行・ログ出力
```powershell
npm i nodemon npm-run-all express-pino-logger pino-colada
```

## jsdom インストール（Webスクレイピング）
Webスクレイピングするのに jsdom をインストール
```powershell
npm i jsdom
```

## バックエンドサーバ作成 (express) 
実行モジュールはどこに作ってもOK
ここではプロジェクト直下に `server` 作ってその下にモジュール `server.js` を作成

1. プロジェクト直下に `server` フォルダ作成
1. `server` フォルダ直下に `server.js` 作成
    ```javascript:server.js
    // ライブラリ読込
    const express = require('express');
    const pino = require('express-pino-logger');
    const { JSDOM } = require('jsdom');

    // express作成
    const app = express();
    app.use(express.urlencoded({ extended: false }));
    app.use(pino());

    // 簡易なJSON返すAPI
    app.get('/api/sample1', async (req, res) => {
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify({ sample: 'Hello world!' }));
    });

    // 簡易にWebスクレイピングしてJSON返すAPI
    app.get('/api/sample2', async (req, res) => {
      // fetch でサイトとってきて dom を生成
      const url = 'https://news.yahoo.co.jp/';
      const urlRes = await fetch(url);
      const urlHtml = await urlRes.text();
      const dom = new JSDOM(urlHtml);
      // ほしい情報を querySelector などを使って取得
      const imgSrcs = [...dom.window.document.querySelectorAll('section img')].map(
        (img) => img.src
      );

      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify({ sample: imgSrcs }));
    });

    // サーバ起動
    app.listen(3001, () =>
      console.log('Express server is running on localhost:3001')
    );

    ```

## サーバへのプロキシ設定
package.json にプロキシの設定を追加
追加する場所はどこでもOK
```json:package.json
  "proxy": "http://localhost:3001"
```

## フロント・バックエンドの起動スクリプト追加
package.json にバックエンド(server.js)の起動、フロント・バックエンドの同時起動のスクリプトを追加
- `npm run server` : バックエンド起動するときのコマンド
- `npm run start-all` : フロント・バックエンド同時起動するときのコマンド

```diff_json:package.json
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
-    "eject": "react-scripts eject"
+    "eject": "react-scripts eject",
+    "server": "nodemon server/server.js | pino-colada",
+    "start-all": "run-p server start"
  },
```

## フロント(react) からAPI実行の簡易サンプル
App.js にバックエンドの以下のAPI実行するサンプルコードを記載
元のコードあるけど全部、置き換えてOK
- /api/sample1 : 簡易な JSON を返すAPI
- /api/sample2 : 簡易にWebスクレイピングした値を設定した JSON を返すAPI

```javascript:App.js
import { useState } from 'react';

function App() {
  const [sample1, setSample1] = useState('');
  const [sample2, setSample2] = useState('');
  // api/sample1 実行して sample1 に設定する処理
  const getApiSample1 = async () => {
    const json = await (await fetch('/api/sample1')).json();
    setSample1(json.sample);
  };
  // api/sample2 実行して sample2 に設定する処理
  const getApiSample2 = async () => {
    const json = await (await fetch('/api/sample2')).json();
    const imgs = json.sample.map((src, i) => {
      return (
        <>
          <img src={src} alt={i}></img>
        </>
      );
    });
    setSample2(imgs);
  };

  return (
    <>
      <button onClick={getApiSample1}>get api sample1</button>
      <p>{sample1}</p>
      <button onClick={getApiSample2}>get api sample2</button>
      <p>{sample2}</p>
    </>
  );
}

export default App;

```

## 実行確認
フロント・バックエンドを同時実行で以下の画面がブラウザで上がってくる
あがってこないときは htttp://localhost:3000 にブラウザでアクセス

- 実行コマンド
```powershell
npm run start-all
```

- こんな感じになればOK(モザイクは通常かからないです)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51758/9ea61035-9d80-133f-942c-962c0d3ecd9f.png)



# ※React実行でエラーが出る場合
開発を繰り返しているうちに何故か React の実行で以下のエラーが出るようになったので対応を記載
理由については不明．．．
最初からこっちの方が安定するかも．．．
React でもこんな感じで紹介してる方法です
https://create-react-app.dev/docs/proxying-api-requests-in-development/

```terminal:エラーログ
Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
 - options.allowedHosts[0] should be a non-empty string.
```

## http-proxy-middleware インストール

```powershell
npm i http-proxy-middleware
```

## package.json からプロキシ設定削除
```diff_json:package.json
-  "proxy": "http://localhost:3001"
```

## setupProxy.js 作成
フォルダ src 直下に `setupProxy.js` を作成
```javascript:src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = (app) => {
  app.use(
    '/api/*',
    createProxyMiddleware({
      target: 'http://localhost:3001',
      changeOrigin: true,
    })
  );
};
```

## 実行確認
実行方法はいままでと同様、エラーなく実行できればOK

```powershell
npm run start-all
```