0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

趣味でアルバム検索アプリ(Webアプリ)をnode js (Docker Express & React + Redux)で実装しました

Last updated at Posted at 2026-01-15

#1. はじめに

アルバム検索アプリをnode jsで作成しました。

album-search-app-api:
アルバムデータを取得するAPIサーバー
Model/Router (node js + Docker + Express + Sequelize)
http://localhost:3000

album-search-app:
 レンダリングサーバー
 React js + Redux
 http://localhost:5173/
MySQL:
 DBテーブル:
   artists
   albums
   songs
   albumsongs

#2. album-search-app-api(アルバムデータを取得するAPIサーバー)

node js + Docker + Express + Sequelize

■プロジェクトを作ろう

express --view=[テンプレートエンジン] [プロジェクト名]

テンプレートエンジン:ejs
プロジェクト名:album-search-app-api

D:\proj\JavaScript\node_js>

express --view=ejs album-search-app-api

ライブラリをインストールします。

cd album-search-app-api
npm install

この段階でWebアプリケーションとして最低限の構成ができたので実際に動かしてみましょう。

npm start

(D:\proj\JavaScript\node_js\album-search-app-api>npm start)

ブラウザで3000ポートを指定して画面が表示されれば成功
http://localhost:3000

■ORM環境を追加しよう
※ここではDBはMySQLを使用する前提で記載します。

MySQLへの接続用ライブラリとsequelizeの本体をインストールします。

npm install mysql2 sequelize

sequelize-cliでORM用のディレクトリとファイルを生成

npx sequelize-cli init

トップフォルダ/album-search-app-api/配下にconfig, migrations, models, seedersができる

DBの接続情報は/album-search-app-api/config/config.jsonに記載します。

///////////////////////////////////////////
自分の環境用設定

/album-search-app-api/config/config.json

{
  "development": {
    "username": "user0",
    "password": "on3tts16",
    "database": "database_development",
    "timezone": "+09:00",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "user0",
    "password": "on3tts16",
    "database": "database_test",
    "timezone": "+09:00",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "user0",
    "password": "on3tts16",
    "timezone": "+09:00",
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

///////////////////////////////////////

■マイグレーションをしてみよう

〇Artistテーブルの構成

物理名 型
id int
name varchar(300)
imgUrl varchar(300)
createdAt datetime
updatedAt datetime

テーブルを作成
Artistテーブルのモデルを作成します。

npx sequelize-cli model:generate --name Artist --attributes name:string,imgUrl:string

※--attributesの後に項目と型指定する
※id、CreatedAt、UpdatedAtのフィールドが自動作成される

/album-search-app-api/models/artist.js
/album-search-app-api/migrations/xxxxxxxxxxxxxx-create-artist.js
というファイルが生成される。

■Migrationの実行

npx sequelize-cli db:migrate

■Seedも使ってデータを登録してみる
Seedファイルを生成します

npx sequelize-cli seed:generate --name test-artist

/album-search-app-api/seeders/xxxxxxxxxxxxxx-test-artist.js
というファイルが生成される。

このファイルの内容を以下のように変更する

/album-search-app-api/seeders/xxxxxxxxxxxxxx-test-artist.js
'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    const now = new Date();
    return queryInterface.bulkInsert('Artists', [
      { name: 'BUMP OF CHICKEN',  imgUrl: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRcEjWJw0bWGNkw-K2VPxCcD6qlM3m_TmIAmnJRNWJ3r7PNYuAx', createdAt: now, updatedAt: now},
      { name: 'androp',  imgUrl: 'https://skream.jp/interview/2014/08/04/images/androp.jpg', createdAt: now, updatedAt: now},
      { name: 'Ivy to Fraudulent Game',  imgUrl: 'http://eggman.jp/eggwp/wp-content/uploads/2023/08/953467b51c44839bc096aa6f4564f8e6.png', createdAt: now, updatedAt: now},
    ], {});
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('Artists', null, {});
  }
};

Seedの実行

npx sequelize-cli db:seed:all

〇Albumテーブルの構成

id int
name varchar(300)
artistId int
imgUrl varchar(300)
productUrl(300)
createdAt datetime
updatedAt datetime

npx sequelize-cli model:generate --name Album --attributes name:string,artistId:integer,imgUrl:string,productUrl:string

npx sequelize-cli db:migrate

npx sequelize-cli seed:generate --name test-album

/album-search-app-api/seeders/xxxxxxxxxxxxxx-test-album.js
というファイルが生成される。

このファイルの内容を以下のように変更する

/album-search-app-api/seeders/xxxxxxxxxxxxxx-test-album.js
'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    const now = new Date();
    return queryInterface.bulkInsert('Albums', [
      { name: 'Butterflies', artistId: 24, imgUrl: 'https://m.media-amazon.com/images/I/61BrTeaqAFL._AC_SX450_.jpg', createdAt: now, updatedAt: now},
    ], {});
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('Albums', null, {});
  }
};

Seedの実行

npx sequelize-cli db:seed:all

〇Songテーブルの構成

id int
name varchar(300)
artistId int
videoUrl varchar(300)
createdAt datetime
updatedAt datetime

npx sequelize-cli model:generate --name Song --attributes name:string,artistId:integer,videoUrl:string

npx sequelize-cli db:migrate

npx sequelize-cli seed:generate --name test-song

/album-search-app-api/seeders/xxxxxxxxxxxxxx-test-song.js
というファイルが生成される。

このファイルの内容を以下のように変更する

/album-search-app-api/seeders/xxxxxxxxxxxxxx-test-song.js
'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    const now = new Date();
    return queryInterface.bulkInsert('Songs', [
      { name: 'GO', artistId: 24, videoUrl: 'https://aaaa111.xxx', createdAt: now, updatedAt: now},
      { name: 'Hello,world!', artistId: 24, videoUrl: 'https://aaaa222.xxx', createdAt: now, updatedAt: now},
      { name: 'Butterfly', artistId: 24, videoUrl: 'https://aaaa333.xxx', createdAt: now, updatedAt: now},
      { name: '流星群', artistId: 24, videoUrl: 'https://aaaa444.xxx', createdAt: now, updatedAt: now},
      { name: '宝石になった日', artistId: 24, videoUrl: 'https://aaaa555.xxx', createdAt: now, updatedAt: now},
      { name: 'コロニー', artistId: 24, videoUrl: 'https://aaaa666.xxx', createdAt: now, updatedAt: now},
      { name: '大我慢大会', artistId: 24, videoUrl: 'https://aaaa777.xxx', createdAt: now, updatedAt: now},
      { name: '孤独の合唱', artistId: 24, videoUrl: 'https://aaaa888.xxx', createdAt: now, updatedAt: now},
      { name: 'You were here', artistId: 24, videoUrl: 'https://aaaa999.xxx', createdAt: now, updatedAt: now},
      { name: 'ファイター', artistId: 24, videoUrl: 'https://aaaa10101010.xxx', createdAt: now, updatedAt: now},
    ], {});
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('Songs', null, {});
  }
};

Seedの実行

npx sequelize-cli db:seed:all

〇AlbumSongテーブルの構成

id int
albumId int
songOrderNo int
songId int
createdAt datetime
updatedAt datetime

npx sequelize-cli model:generate --name AlbumSong --attributes albumId:integer,songOrderNo:integer,songId:integer

npx sequelize-cli db:migrate

npx sequelize-cli seed:generate --name test-albumsong

/album-search-app-api/seeders/xxxxxxxxxxxxxx-test-albumsong.js
というファイルが生成される。

このファイルの内容を以下のように変更する

/album-search-app-api/seeders/xxxxxxxxxxxxxx-test-albumsong.js
'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    const now = new Date();
    return queryInterface.bulkInsert('AlbumSongs', [
      { albumId: 1, songOrderNo: 1, songId: 1, createdAt: now, updatedAt: now},
      { albumId: 1, songOrderNo: 2, songId: 2, createdAt: now, updatedAt: now},
      { albumId: 1, songOrderNo: 3, songId: 3, createdAt: now, updatedAt: now},
      { albumId: 1, songOrderNo: 4, songId: 4, createdAt: now, updatedAt: now},
      { albumId: 1, songOrderNo: 5, songId: 5, createdAt: now, updatedAt: now},
      { albumId: 1, songOrderNo: 6, songId: 6, createdAt: now, updatedAt: now},
      { albumId: 1, songOrderNo: 7, songId: 7, createdAt: now, updatedAt: now},
      { albumId: 1, songOrderNo: 8, songId: 8, createdAt: now, updatedAt: now},
      { albumId: 1, songOrderNo: 9, songId: 9, createdAt: now, updatedAt: now},
      { albumId: 1, songOrderNo: 10, songId: 10, createdAt: now, updatedAt: now},
    ], {});
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('AlbumSongs', null, {});
  }
};

Seedの実行

npx sequelize-cli db:seed:all

■エントリーポイント

/album-search-app-api/app.js

/album-search-app-api/app.js
var createError = require('http-errors');
//var express = require('express');
const express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
//var usersRouter = require('./routes/users');
var artistListRouter = require('./routes/artistlist');
var artistRouter = require('./routes/artist');
var albumRouter = require('./routes/album');

//var app = express();
const app = express();
////////////
const cors = require('cors');

//////////////
app.use(cors());
/*
app.use(cors({
    origin: 'http://localhost:5173', //アクセス許可するオリジン
    credentials: true, //レスポンスヘッダーにAccess-Control-Allow-Credentials追加
    optionsSuccessStatus: 200 //レスポンスstatusを200に設定
}));
*/
////////////

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
//app.use('/users', usersRouter);
app.use('/artistlist', artistListRouter);
app.use('/artist', artistRouter);
app.use('/album', albumRouter);

module.exports = app;

〇index (/)
/album-search-app-api/routes/index.js

/album-search-app-api/routes/index.js
var express = require('express');
var router = express.Router();

/* GET home page. */
/*
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});
*/
router.get('/', function(req, res, next) {
    //const sharedData = {};
    const sharedData = {
        requestName: "index",
        ///////////////////////
        // これは必要
        artist: {
            artistName: "1111111111111",
        },
        ///////////////////////
        attribute1: "aaaaa",
        attribute2: "bbbbb",
    };

    //res.render('index', { title: 'Express' });
    res.status(200).json(sharedData);
});

module.exports = router;

実行!
npm start

ブラウザで3000ポートを指定して画面が表示されれば成功
http://localhost:3000

画面:
{"requestName":"index","artist":{"artistName":"1111111111111"},"attribute1":"aaaaa","attribute2":"bbbbb"}
JSONが表示された

〇artistlist (/artistlist)

/album-search-app-api/routes/artistlist.js

/album-search-app-api/routes/artistlist.js
var express = require('express');
var router = express.Router();

var db = require('../models/');
// URLクエリーパラメータ指定の場合
//http://localhost:3000/artistlist?key=BUMP%20OF%20CHICKEN
router.get('/', (req, res, next) => {
    //http://localhost:3000/artistlist/:keyWord の場合
    //const keyWord = req.params.keyWord;
    //console.log("req:");
    //console.log(req);
    const params = req.params;
    const query = req.query;
    console.log("query:");
    console.log(query);
    console.log("query.key:" + query.key); //?key=xxxx
    const keyWord = query.key;
    
    //const keyWord = "BUMP OF CHICKEN";
    console.log("keyWord:" + keyWord);
    //

    /////////////////////////////////////////
    // Sequelizeのモデルを使ってデータを取得する
    db.Artist.findAll({
        where: {
            name: keyWord
        }
    }).then(rows => {
       if (!rows || rows[0] == null) {
            console.log("db.Artist.findAll: データを取得できませんでした");

            //const sharedData = {};
            res.status(200).json(sharedData);
        }
        else {
            console.log("db.Artist.findAll rows");
            console.log(rows);
            console.log("row[0].dataValues");
            console.log(rows[0].dataValues);

            /////////////////////////////////////////
            //const sharedData = {};
            /*
            const sharedData = {
                requestName: "artistlist",
                keyWord: keyWord,
                artistList: [
                    { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN", artistName: "BUMP OF CHICKEN" },
                    { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN11111", artistName: "BUMP OF CHICKEN11111" },
                    { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN22222", artistName: "BUMP OF CHICKEN22222" },
                    { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN33333", artistName: "BUMP OF CHICKEN33333" },
                    { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN44444", artistName: "BUMP OF CHICKEN44444" },
                    { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN55555", artistName: "BUMP OF CHICKEN55555" },
                ],
            };
            */
           const sharedData = {
                requestName: "artistlist",
                keyWord: keyWord,
                artistList: [],
           };

           sharedData.artistList = 
                rows.map((row) => {
                    //return { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN", artistName: "BUMP OF CHICKEN" };

                    const id = row.id;
                    const name = row.name;
                    const imgUrl = row.imgUrl;
                    const artistPageUrl = "/artist/" + id;
                    let artist = {
                        artistId: id,
                        artistName: name,
                        artistImgUrl: imgUrl, 
                        artistPageUrl: artistPageUrl,
                    };
                    return artist;
                });

            //res.set({ 'Access-Control-Allow-Origin': '*' }); // ここでヘッダーにアクセス許可の情報を追加
	        //res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
	        //res.setHeader('Access-Control-Allow-Origin', '*');

            //res.render('index', { title: 'Express' });
            res.status(200).json(sharedData);
            /////////////////////////////////////////

        }
    }).catch((error) => console.log(error.message))
    .finally(() => console.log("👿 最後に実行される"));
    /////////////////////////////////////////
    
});

module.exports = router;

/album-search-app-api/routes/index.js

/routes/index.js
var express = require('express');
var router = express.Router();

/* GET home page. */
/*
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});
*/
router.get('/', function(req, res, next) {
    //const sharedData = {};
    const sharedData = {
        requestName: "index",
        ///////////////////////
        // これは必要
        artist: {
            artistName: "1111111111111",
        },
        ///////////////////////
        attribute1: "aaaaa",
        attribute2: "bbbbb",
    };

    //res.render('index', { title: 'Express' });
    res.status(200).json(sharedData);
});

module.exports = router;

〇artistlist(/artistlist?key=[keyWord])
/album-search-app-api/routes/artistlist.js

/routes/artistlist.js
var express = require('express');
var router = express.Router();

var db = require('../models/');

// URLクエリーパラメータ指定の場合
//http://localhost:3000/artistlist?key=BUMP%20OF%20CHICKEN
router.get('/', (req, res, next) => {
    //http://localhost:3000/artistlist/:keyWord の場合
    //const keyWord = req.params.keyWord;
    const params = req.params;
    const query = req.query;
    console.log("query:");
    console.log(query);
    console.log("query.key:" + query.key); //?key=xxxx
    //////////////////////////////////
    const keyWord = query.key;
    
    //const keyWord = "BUMP OF CHICKEN";
    console.log("keyWord:" + keyWord);

    /////////////////////////////////////////
    // Sequelizeのモデルを使ってデータを取得する
    db.Artist.findAll({
        where: {
            name: keyWord
        }
    }).then(rows => {
       if (!rows || rows[0] == null) {
            console.log("db.Artist.findAll: データを取得できませんでした");

            //const sharedData = {};
            res.status(200).json(sharedData);
        }
        else {
            console.log("db.Artist.findAll rows");
            console.log(rows);
            console.log("row[0].dataValues");
            console.log(rows[0].dataValues);

            /////////////////////////////////////////
            //const sharedData = {};
            /*
            const sharedData = {
                requestName: "artistlist",
                keyWord: keyWord,
                artistList: [
                    { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN", artistName: "BUMP OF CHICKEN" },
                    { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN11111", artistName: "BUMP OF CHICKEN11111" },
                    { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN22222", artistName: "BUMP OF CHICKEN22222" },
                    { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN33333", artistName: "BUMP OF CHICKEN33333" },
                    { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN44444", artistName: "BUMP OF CHICKEN44444" },
                    { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN55555", artistName: "BUMP OF CHICKEN55555" },
                ],
            };
            */
           const sharedData = {
                requestName: "artistlist",
                keyWord: keyWord,
                artistList: [],
           };

           sharedData.artistList = 
                rows.map((row) => {
                    //return { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN", artistName: "BUMP OF CHICKEN" };

                    const id = row.id;
                    const name = row.name;
                    const imgUrl = row.imgUrl;
                    const artistPageUrl = "/artist/" + id;
                    let artist = {
                        artistId: id,
                        artistName: name,
                        artistImgUrl: imgUrl, 
                        artistPageUrl: artistPageUrl,
                    };
                    return artist;
                });

            //res.set({ 'Access-Control-Allow-Origin': '*' }); // ここでヘッダーにアクセス許可の情報を追加
	        //res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
	        //res.setHeader('Access-Control-Allow-Origin', '*');

            //res.render('index', { title: 'Express' });
            res.status(200).json(sharedData);
            /////////////////////////////////////////

        }
    }).catch((error) => console.log(error.message))
    .finally(() => console.log("👿 最後に実行される"));
    /////////////////////////////////////////
    
});

module.exports = router;

〇artist(/artist/:artistId)
/routes/artist.js

/routes/artist.js
var express = require('express');
var router = express.Router();

var db = require('../models/');

router.get('/', (req, res, next) => {
    const sharedData = {};

    res.status(200).json(sharedData);
});

//http://localhost:3000/artist/27
router.get('/:artistId', function(req, res, next) {
    const artistId = req.params.artistId;
    console.log("artistId:" + artistId);

    /*
    //const sharedData = {};
    const sharedData = {
        requestName: "artist",
        artist: {
            artistName: "BUMP OF CHICKEN",
            artistId: artistId,
            artistImgUrl: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRcEjWJw0bWGNkw-K2VPxCcD6qlM3m_TmIAmnJRNWJ3r7PNYuAx",
            pageNo: 1,
            albumList: [
                { detailPageUrl: "/album/Butterflies", productTitle: "Butterflies" },
                { detailPageUrl: "/album/RAY", productTitle: "RAY" }
            ]
        },
    };
    */

    /////////////////////////////////
    const sharedData = {
        requestName: "artist",
        artistId: artistId,
        artist: {},
    };
    /////////////////////////////////
    db.Artist.findAll({
        where: {
            id: artistId
        }
    }).then(rows => {
        if (!rows || rows[0] == null) {
            console.log("db.Artist.findAll: データを取得できませんでした");

            //const sharedData = {};
            res.status(200).json(sharedData);
        }
        else {
            ////////////////////////////////////
            console.log("db.Artist.findAll rows");
            console.log(rows);
            console.log("row[0].dataValues");
            console.log(rows[0].dataValues);
            let row = rows[0];

            ///////////////////////////////////
            const id = row.dataValues.id;
            const name = row.dataValues.name;
            const imgUrl = row.dataValues.imgUrl;
            const artistPageUrl = "/artist/" + id;
            let artist = {
                artistId: id,
                artistName: name,
                artistImgUrl: imgUrl, 
                artistPageUrl: artistPageUrl,
                albumList: [],
            };

            ///////////////////
            console.log("artist:");
            console.log(artist);
            sharedData.artist = artist;

            ////////////////////////////////////////////
            db.Album.findAll({
                where: {
                    artistId: artistId
                }
            }).then(rows => {
                ///////////////////////////
                if (!rows || rows[0] == null) {
                    console.log("db.Album.findAll: データを取得できませんでした");

                    //const sharedData = {};
                    res.status(200).json(sharedData);
                }
                else {
                    ///////////////////////////////
                    console.log("db.Album.findAll rows");
                    console.log(rows);
                    //console.log("row[0].dataValues");
                    //console.log(rows[0].dataValues);
                    //let row = rows[0];

                    /////////////////////////////////
                    artist.albumList = 
                        rows.map((row) => {
                            //return { detailPageUrl: "/album/Butterflies", productTitle: "Butterflies" };

                            ////const id = row.dataValues.id;
                            ////const name = row.dataValues.name;
                            ////const imgUrl = row.dataValues.imgUrl;
                            ////const albumPageUrl = "/album/" + id;
                            const id = row.id;
                            const name = row.name;
                            const imgUrl = row.imgUrl;
                            const albumPageUrl = "/album/" + id;
                            let album = {
                                albumId: id,
                                productTitle: name,
                                detailPageUrl: albumPageUrl
                            };
                            return album;
                            //
                        });
                    

                    console.log("artist:");
                    console.log(artist);

                    sharedData.artist = artist;

                    //res.render('index', { title: 'Express' });
                    res.status(200).json(sharedData);

                }
                ///////////////////////////////////////////
            });
        }

        ////res.render('index', { title: 'Express' });
        //res.status(200).json(sharedData);

    });
    /////////////////////////////////////////

});

module.exports = router;

〇album (/album/:albumId)
/album-search-app-api/routes/album.js

/album-search-app-api/routes/album.js
var express = require('express');
var router = express.Router();


var db = require('../models/');

router.get('/', (req, res, next) => {
    const sharedData = {};

    res.status(200).json(sharedData);
});

//http://localhost:3000/album/1
router.get('/:albumId', function(req, res, next) {
    const albumId = req.params.albumId;
    console.log("albumId:" + albumId);

    //const sharedData = {};
    /*
    const sharedData = {
        requestName: "album",
        album: {
            albumId: albumId,
            productTitle: "Butterflies",
            imgUrl: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRcEjWJw0bWGNkw-K2VPxCcD6qlM3m_TmIAmnJRNWJ3r7PNYuAx",
            productUrl: "https://www.amazon.co.jp/Butterflies-%E9%80%9A%E5%B8%B8%E7%9B%A4-BUMP-CHICKEN/dp/B018HYIPFW/",
            artistName: "BUMP OF CHICKEN",
            artistPageUrl: "/artist/BUMP%20OF%20CHICKEN/1",
            artistPageNo: 1,
            songList: [
                {songName: "GO", songUrl: "/song/1"},
                {songName: "Hello,world!", songUrl: "/song/2"},
                {songName: "Butterfly", songUrl: "/song/3"},
                {songName: "流星群", songUrl: "/song/4"},
                {songName: "宝石になった日", songUrl: "/song/5"},
                {songName: "コロニー", songUrl: "/song/6"},
                {songName: "大我慢大会", songUrl: "/song/7"},
                {songName: "孤独の合唱", songUrl: "/song/8"},
                {songName: "You were here", songUrl: "/song/9"},
                {songName: "ファイター", songUrl: "/song/10"}
           ]
        }
    };
    */
    //////////////////
    const sharedData = {
        requestName: "album",
        album: {
            albumId: albumId,
            productTitle: "",
            imgUrl: "",
            productUrl: "",
            artistName: "",
            artistPageUrl: "",
            artistPageNo: 0,
            songList: [
                //{songName: "GO", songUrl: "/song/1"},
           ]
        }
    };

    /////////////////////////////////
    fetchAlbum(req, res, sharedData, albumId);

});

function fetchAlbum(req, res, sharedData, albumId) {
    console.log("fetchAlbum");

    //yyyyyyyyyyyyyyyy
    db.Album.findAll({
        where: {
            id: albumId
        }
    }).then(rows => {
        //xxxxxxxxxxxxxxxxxxxxx
        if (!rows || rows[0] == null) {
            console.log("db.Album.findAll: データを取得できませんでした");
    
            //const sharedData = {};
            res.status(200).json(sharedData);
        }
        else {
            ////////////////////////////////////
            console.log("db.Album.findAll rows");
            console.log(rows);
            console.log("row[0].dataValues");
            console.log(rows[0].dataValues);
            let row = rows[0];

            ///////////////////////////////////
            const id = row.dataValues.id;
            console.log("id:" + id + "  albumId:" + albumId + "  同じはず");
            const name = row.dataValues.name;
            const albumPageUrl = "/album/" + albumId;
            const artistId = row.dataValues.artistId;
            const artistPageUrl = "/artist/" + artistId;
            const imgUrl = row.dataValues.imgUrl;
            const productUrl = row.dataValues.productUrl;

            /*
            album: {
                albumId: albumId,
                productTitle: "",
                imgUrl: "",
                productUrl: "",
                artistName: "",
                artistPageUrl: "",
                albumPageNo: 1,
                songList: [
                    //{songName: "GO", songUrl: "/song/1"},
               ]
            }
            */
            let album = {
                albumId: id,
                productTitle: name,
                imgUrl: imgUrl,
                productUrl: productUrl,
                artistId: id,
                artistName: "",
                artistPageUrl: artistPageUrl,
                albumPageNo: 0,
                songIdList: [], // テンポラリーデータ
                songList: [],
            };

            ///////////////////
            console.log("album:");
            console.log(album);
            sharedData.album = album;

            ////////////////////////////////////////////
            fetchAlbumSong(req, res, sharedData, albumId, album);
        }
        //xxxxxxxxxxxxxxxxxxxxx

    }).catch((error) => console.log(error.message))
    .finally(() => console.log("👿 最後に実行される"));
    //yyyyyyyyyyyyyyyy
}

function fetchAlbumSong(req, res, sharedData, albumId, album) {
    console.log("fetchAlbumSong");
    //222222222222222
    db.AlbumSong.findAll({
        where: {
            albumId: albumId
        }
    }).then((rows) => {
        //qqqqqqqqqqqqqqqqqqqqq
        if (!rows || rows[0] == null) {
            console.log("db.AlbumSong.findAll: データを取得できませんでした");

            //const sharedData = {};
            res.status(200).json(sharedData);
        }
        else {
            //pppppppppppppppppppppp
            ///////////////////////////////
            console.log("db.AlbumSong.findAll rows");
            console.log(rows);
            //console.log("row[0].dataValues");
            //console.log(rows[0].dataValues);
            //let row = rows[0];

            /////////////////////////////////
            const albumSongIdList = 
                rows.map((row) => {
                    //return { albumId: 1, songOrderNo: 1, songId: 1};

                    
                    //const id = row.id; //これは単なるインデックス 意味なし
                    const albumId = row.albumId;
                    const songOrderNo = row.songOrderNo;
                    const songId = row.songId;
                    
                    let albumSongId = {
                        albumId: albumId,
                        songOrderNo: songOrderNo,
                        songId: songId
                    };
                    return albumSongId;
                                    
                });
                    

            console.log("albumSongIdList:");
            console.log(albumSongIdList);

            album.songIdList = albumSongIdList;

            sharedData.album = album;

            ////////////////////////////////////////////
            //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            let albumSongList = [];
            let albumSongOrderIndex = 0;
            let songIdListLen = albumSongIdList.length;
            console.log("songIdListLen:" + songIdListLen);

            const albumSongPromiseList = albumSongIdList.map(async(row, index) => {
                //return {id: 1, name: "GO", artistId: 24, videoUrl: "https://aaaa111.xxx"};

                const songId = row.songId;
                console.log("albumSongList.map:");
                console.log("songId:" + songId);
                console.log("index:" + index);
                let song = await fetchSong(req, res, sharedData, album, songId, albumId);
                
                //let song = {};
                //fetchSong(req, res, sharedData, album, songId, albumId).then(result => {
                //    song = result;
                //});
                console.log("fetchSong await");
                console.log("------------song:");
                console.log(song);
                console.log("albumSongOrderIndex:" + albumSongOrderIndex);

                ////////////////////
                albumSongList[albumSongOrderIndex] = song;
                albumSongOrderIndex++;
                ////////////////////
                if (albumSongOrderIndex == songIdListLen) {
                    //2222222222
                    console.log("albumSongList:222222222");
                    console.log(albumSongList);
                    album.songList = albumSongList;
                    
                    sharedData.album = album;

                    //res.render('index', { title: 'Express' });
                    res.status(200).json(sharedData);
                }

                return song;
            });

            /*
            //1111111111
            console.log("albumSongList:111111111");
            console.log(albumSongList);
            album.songList = albumSongList;
            //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

            sharedData.album = album;

            //res.render('index', { title: 'Express' });
            res.status(200).json(sharedData);
            */
            //pppppppppppppppppppppp
        }
        //111111111111111111111111

    });
    //222222222222222
}

async function fetchSong(req, res, sharedData, album, songId, albumId) {//11111111111
    let song = {
        id: songId,
        name: "",
        artistId: 0,
        videoUrl: ""};


    ////////////////////////////////////////
    let rows = await db.Song.findAll({
        where: {
            id: songId
        }
    });

    /////////////////////////////////////
    if (!rows || rows[0] == null) {
        console.log("db.Song.findAll: データを取得できませんでした");
    }
    else {
        ///////////////////////////////                
        //console.log("db.Song.findAll rows");
        //console.log(rows);
        //console.log("row[0].dataValues");
        //console.log(rows[0].dataValues);
                
        let row = rows[0];
        let data = rows[0].dataValues;
                
        //333333333333333333333333333
        //song = 
        //    { id: 1, name: "GO", artistId: 24, videoUrl: "https://aaaa111.xxx"};
                        
        const songId = data.id;
        const songName = data.name;
        const artistId = data.artistId;
        const videoUrl = data.videoUrl;
        const songOrderNo = 0; //あとまわし
        const songUrl = "/song/" + songId;
                        
        //{songName: "GO", songUrl: "/song/1"},
        song = {
            songId: songId,
            songName: songName,
            albumId: albumId,
            songOrderNo: songOrderNo,
            songUrl: songUrl,
            videoUrl: videoUrl,
            artistId: artistId
        };
                
                
        //console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!song:");
        //console.log(song);
                
        //333333333333333333333333333
        //
    }
        
    console.log("xxxxxxxxxsong:");
    console.log(song);
    return song;//111111111111111
}//11111111111111


module.exports = router;

#3. album-search-app(Webアプリ レンダリングサーバー)

node js + React + Redux

■Reactのインストール

Node.jsとnpmがインストール済みとします。
reate React Appを実行する方法は古い方法です。
2023年にサポートが終わっており利用をお勧めしません。
代わりにViteというものを利用してインストールを行います。

●Visual Studio Codeを開き、フォルダを開くで開発に使いたいフォルダを開きます。
その状態で画面上部のタブから「ターミナル」を開き、「新しいターミナル」をクリックします。

フォルダをD:\proj\JavaScript\node_js\react-app-testとして進める

●画面下部にターミナルというツールが表示されるので、下記のコマンドを入力してください。

npm create vite@latest

※エラーがでる
このシステムではスクリプトの実行が無効になっているため、ファイル C:\Program Files\nodejs\npm.ps1 を読み込むことができません。

※解決策
https://ydk.vc/vscode-nodejs-scripts-about-execution-policies/
PowerShellの実行ポリシーをRemoteSigned以上にすれば良いっぽい。

ターミナルで以下を実行
許可するコマンドを入力して実行。

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

とりあえずコマンドで権限変わってるか確認。RemoteSignedが表示されたらOK。

Get-ExecutionPolicy

●再度実行してみる

npm create vite@latest

エラーは出なくなった

Need to install the following packages:
create-vite@6.1.1
Ok to proceed? (y)
create-viteをインストールしますか?と聞かれるのでYesと答えます。
? Project name: ›
ここで作成するフォルダの名前を決めます(英語が良いです)

✔ Project name: … album-search-app
? Select a framework: › - Use arrow-keys. Return to submit.
Vanilla
Vue
❯ React
Preact
Lit
Svelte
Solid
Qwik
Angular
Others
どれで開発するか聞かれるため、今回はReactを選択します。
上下矢印で選択

? Select a variant: › - Use arrow-keys. Return to submit.
TypeScript
❯ TypeScript + SWC
JavaScript
JavaScript + SWC
React Router v7 ↗
開発に使う言語等を聞かれます。
https://monotein.com/blog/react-vite-how-to-use
JavaScriptとだけ書かれているものを選択します。

Use rolldown-vite (Experimental)?:
│ ○ Yes
│ ● No

これはまだ実験段階の機能なので「No」
次に下記の質問が出ますが、これも「No」を選び、「Enter」キーを押しましょう。

◆ Install with npm and start now?
│ ○ Yes / ● No

これで完了です
album-search-appフォルダが生成されています。

「Vite + React」では手動で必要なインストールする必要があります。

なので、VS Code上部メニューバーの「Terminal」から「New Terminal」を選び、
VS Code下部にターミナルを出してください。

そこに、パッケージをインストールする下記コマンドを打ち、「Enter」で実行しましょう。

cd album-search-app
npm install

node_modulesが作成されます。これはパッケージ関係のフォルダです。
これで起動コマンドであるnpm run devを実行すると、「Vite + React」が起動しますが
その前にまっさらな状態にするため「Vite + React」のクリーンアップを行います

srcフォルダを開き、その中のassetsフォルダ、index.cssを削除してください。
さらにpublicフォルダ内のvite.svgも削除しましょう。

App.css, App.jsxも削除。

■エントリーポイント
/album-search-app/index.html

/album-search-app/index.html
<!DOCTYPE html>
<html>
  <head>
    <link href="style.css" rel="stylesheet" />
    <meta charset="utf-8" />
    <title>アルバム検索アプリ</title>

    <script>
    window._sharedData = {
      /*
      artist: {
        artistName: "",
        artistImgUrl: "",
        pageNo: 1,
        albumList: [
          //{ detailPageUrl: "", productTitle: "" }
        ]
      },
      album: {
        productTitle: "",
        imgUrl: "",
        productUrl: "",
        artistName: "",
        artistPageUrl: "",
        artistPageNo: 1,
        songList: [
          //{ songName: ""}
        ]
      }
      */
      artistList: [
        { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN", artistName: "BUMP OF CHICKEN" },
        { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN11111", artistName: "BUMP OF CHICKEN11111" },
        { artistPageUrl: "/artist/BUMP%20OF%20CHCKEN22222", artistName: "BUMP OF CHICKEN22222" }
      ],
      artist: {
        artistName: "BUMP OF CHICKEN",
        artistImgUrl: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRcEjWJw0bWGNkw-K2VPxCcD6qlM3m_TmIAmnJRNWJ3r7PNYuAx",
        pageNo: 1,
        albumList: [
          { detailPageUrl: "/album/Butterflies", productTitle: "Butterflies" },
          { detailPageUrl: "/album/RAY", productTitle: "RAY" }
        ]
      },
      album: {
        productTitle: "Butterflies",
        imgUrl: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRcEjWJw0bWGNkw-K2VPxCcD6qlM3m_TmIAmnJRNWJ3r7PNYuAx",
        productUrl: "https://www.amazon.co.jp/Butterflies-%E9%80%9A%E5%B8%B8%E7%9B%A4-BUMP-CHICKEN/dp/B018HYIPFW/",
        artistName: "BUMP OF CHICKEN",
        artistPageUrl: "/artist/BUMP%20OF%20CHICKEN/1",
        artistPageNo: 1,
        songList: [
          {songName: "GO", songUrl: "/song/1"},
          {songName: "Hello,world!", songUrl: "/song/2"},
          {songName: "Butterfly", songUrl: "/song/3"},
          {songName: "流星群", songUrl: "/song/4"},
          {songName: "宝石になった日", songUrl: "/song/5"},
          {songName: "コロニー", songUrl: "/song/6"},
          {songName: "大我慢大会", songUrl: "/song/7"},
          {songName: "孤独の合唱", songUrl: "/song/8"},
          {songName: "You were here", songUrl: "/song/9"},
          {songName: "ファイター", songUrl: "/song/10"}
        ]
      }
    }
    </script>

  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

次はmain.jsxの内容を消し、次のコードを書いてください。

/album-search-app/src/main.jsx

/album-search-app/src/main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'

// React Router を使うためのいろいろなもの
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { AppRoutes } from './Routes.jsx'; // Routes.jsxを参照

// React redux
import { Provider } from 'react-redux';
//import store, {history} from './store';
import store from './store';
import history from './history';
import {setArtistList, setArtist, setAlbum} from './actions';

// reactでURLの値を取得する方法
//import { useLocation } from "react-router-dom";

/////////////////////////////////////////////////////////
async function fetchData() {
    /////////////
    let defaultData = {};
    window._sharedData = defaultData;
    console.log("window._sharedData default:");
    console.log(window._sharedData);

    //const pathname = "/artistlist/BUMP%20OF%20CHICKEN";
    const pathname = location.pathname;
    console.log("pathname = " + pathname);

    /////////////

    try {
        //const response = await fetch('http://localhost:3000/artistlist/BUMP%20OF%20CHICKEN');
        //const response = await fetch('http://127.0.0.1:3000/artistlist/BUMP%20OF%20CHICKEN');
        //const url = 'http://127.0.0.1:3000/artistlist/BUMP%20OF%20CHICKEN';
        const apiUrl = 'http://127.0.0.1:3000';

        /////////////
        /* 使えない
        console.log("useLocation() 1");
        const location = useLocation();
        console.log("useLocation() 2s");
        console.log("location:");
        console.log(location);
        */
        console.log("window.location:");
        console.log(window.location);
        //let pathname = window.location.pathname;
        //console.log("pathpame:" + pathname);
        //let href = window.location.href;
        //console.log("href:" + href);
        //hrefはドメインも含まれてしまう
        const path = window.location.pathname + window.location.search + window.location.hash;
        console.log("path:" + path);

        //////////////
        //let url = apiUrl + pathname;
        //let url = apiUrl + href;
        let url = apiUrl + path;

        console.log("正規表現チェック pathname:" + pathname);

        const regex1 = /^\/artistlist\//;
        console.log("regex1:");
        console.log(regex1);
        const match1 = pathname.match(regex1);
        console.log("match:");
        console.log(match1);

        const regex2 = /^\/artist\//;
        console.log("regex2:");
        console.log(regex2);
        const match2 = pathname.match(regex2);
        console.log("match2");
        console.log(match2);

        const regex3 = /^\/album\//;
        console.log("regex3:");
        console.log(regex3);
        const match3 = pathname.match(regex3);
        console.log("match3");
        console.log(match3);

        if (match1 !== null) {
            console.log("match1 OK");
        }
        else if (match2 !== null) {
            console.log("match2 OK");
        }
        else if (match3 !== null) {
            console.log("match3 OK");
        }
        else {
            console.log("NO MATCH NG!!!!!!!!!");
        }
        ///////////////////////////////
        /*
        let url = apiUrl + "/";
        if (pathname === "/") {
            url = apiUrl + "/";
        }
        //else if (pathname === "/artistlist") {
        else if (pathname.match(/^\/artistlist\/[*]+$/)) {
            //url = apiUrl + "/artistlist";
            url = apiUrl + pathname;
        }
        else if (pathname === '/artist') {
            url = apiUrl + "/artist";
        }
        else if (pathname === '/album') {
            url - apiUrl + "/album";
        }
        else {
          console.log("No matched condition ");
        }
        */

        console.log("fetch: url=" + url);
        //
        //const response = await fetch(url);
        // 個々のデータのfetchでは、cacheオプションをno-storeに設定することで、
        // キャッシュを無効ににできます。
        const response = await fetch(url, { cache: 'no-store' });

        const data = await response.json();

        // データを処理するコードをここに記述
        console.log("fetchData: data=");
        console.log(data);

        /////////////
        window._sharedData = data;
        console.log("window._sharedData set:");
        console.log(window._sharedData);
        /////////////

    } catch (error) {
        console.error('リクエストエラー:', error);
    }

    /////////////////////
    mainFunction();

}

/////////////////////////////////////////////////////////
function mainFunction() {
    // Note: window._sharedData  はindex.htmlでセットされる
    let sharedData = window._sharedData;

    console.log("mainFunction: sharedData = ");
    console.log(sharedData);

    //////////////////////////////////
    // エラー
    /*
    if (sharedData == null || sharedData == {}) {

        console.log("sharedData is null or empty");

        return (
            <>
            </>
        );
    }
    */

    //////////////////////////////
    store.dispatch(setArtistList(sharedData.artistList));
    store.dispatch(setArtist(sharedData.artist));
    store.dispatch(setAlbum(sharedData.album));

    console.log('store.getState() = ', store.getState());

    /*
    const baseUrl = process.env.PUBLIC_URL;
    console.log("baseUrl = " + baseUrl);
    */
    const baseUrl = import.meta.env.BASE_URL;
    console.log("baseUrl = " + baseUrl);

    /*
    createRoot(document.getElementById('root')).render(
        <StrictMode>
            <BrowserRouter>
                <AppRoutes />
            </BrowserRouter>
        </StrictMode>
    )
    */

    createRoot(document.getElementById('root')).render(
        <StrictMode>
            <Provider store={store}>
                <BrowserRouter  history={history}>
                    <AppRoutes />
                </BrowserRouter>
            </Provider>
        </StrictMode>
    )
}

/////////////////////////////////////////////////////////
fetchData();

これでクリーンアップが完了して、「Vite + React」開発をはじめる地ならしができました。

■ ルーティング
/album-search-app/src/Routes.jsx

/album-search-app/src/Routes.jsx
import { Routes, Route } from "react-router-dom";
import IndexPage from "./indexPage.jsx"; // indexPage.jsxの読み込み
import ArtistListPage from "./artistListPage.jsx";
import ArtistPage from "./artistPage.jsx";
import AlbumPage from "./albumPage.jsx";

const baseUrl = import.meta.env.BASE_URL;
console.log("baseUrl = " + baseUrl);

export const AppRoutes = () => {
    return (
        <Routes>
            {/* <Route path="/" element={<IndexPage />} />*/}
            <Route path="/" element = {<IndexPage />} />
           {/*<Route path="/artistlist/:keyWord" element={<ArtistListPage />} />*/}
           {/*http://localhost:5173/artistlist?key=BUMP%20OF%20CHICKEN*に変更/ */}
           <Route path="/artistlist" element={<ArtistListPage />} />
           <Route path="/artist/:artistId" element={<ArtistPage />} />
           <Route path="/album/:albumId" element={<AlbumPage />} />
       </Routes>
   )
}

■【React】Reduxを使ってみる
https://zenn.dev/xronotech/articles/bc9ee347f5e2a5

●ReduxとReact-Reduxのインストール

npm install redux react-redux

●アクションの定義
アクションを定義します。

/album-search-app/src/constants/ActionTypes.jsx

/album-search-app/src/constants/ActionTypes.jsx
export const SET_ARTIST_LIST_ACTION = 'SET_ARTIST_LIST_ACTION'
//////////////////////////////////////
export const SET_ARTIST_ACTION = 'SET_ARTIST_ACTION'
export const SET_ARTIST_NAME_ACTION = 'SET_ARTIST_NAME_ACTION'
export const SET_ALBUM_ACTION = 'SET_ALBUM_ACTION'

/album-search-app/src/actions/index.jsx

/album-search-app/actions/index.jsx
import * as types from '../constants/ActionTypes'

/////////////////////////////
const setArtistListAction = artistList => ({
    type: types.SET_ARTIST_LIST_ACTION,
    artistList
})

export const setArtistList = artistList => (dispatch, getState) => {
    dispatch(setArtistListAction(artistList))
}


/////////////////////////////
const setArtistNameAction = artistName => ({
    type: types.SET_ARTIST_NAME_ACTION,
    artistName
})

export const setArtistName = artistName => (dispatch, getState) => {
    dispatch(setArtistNameAction(artistName))
}

const setArtistAction = artist => ({
    type: types.SET_ARTIST_ACTION,
    artist
})

export const setArtist = artist => (dispatch, getState) => {
    dispatch(setArtistAction(artist))
}

const setAlbumAction = album => ({
    type: types.SET_ALBUM_ACTION,
    album
})

export const setAlbum = album => (dispatch, getState) => {
    dispatch(setAlbumAction(album))
}

●リデューサーの作成
リデューサーを作成します。リデューサーは状態とアクションを受け取り、新しい状態を返す関数です。

/album-search-app/src/reducers/index.jsx

/album-search-app/src/reducers/index.jsx
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';

import artistList, * as fromArtistList from './artistList';
import artist, * as fromArtist from './artist';
import album, * as fromAlbum from './album';

export default combineReducers({
    routing: routerReducer,
    artistList,
    artist,
    album
})

export const getArtistList = state => fromArtistList.getArtistList(state.artistList);

export const getArtist = state => fromArtist.getArtist(state.artist);

export const getAlbum = state => fromAlbum.getAlbum(state.album);

/album-search-app/src/reducers/artistlist.jsx

/album-search-app/src/reducers/artistlist.jsx
import {SET_ARTIST_LIST_ACTION} from '../constants/ActionTypes';

const initialState = {
    artistList: [
        //{ artistPageUrl: "", artistName: "" }
    ]
}

const _artistList = (state, action) => {
    switch (action.type) {
        case SET_ARTIST_LIST_ACTION:
            return action.artistList
        default:
            return state
    }
}

export const getArtistList = state =>
    state.artistList

const artistList = (state = initialState, action) => {
    return {
        artistList: _artistList(state.artistList, action)
    }
}

export default artistList

/album-search-app/src/reducers/artist.jsx

/album-search-app/src/reducers/artist.jsx
import {SET_ARTIST_ACTION, SET_ARTIST_NAME_ACTION} from '../constants/ActionTypes';

const initialState = {
    artist: {
        artistName: "",
        artistImgUrl: "",
        pageNo: 1,
        albumList: [
            //{ detailPageUrl: "", productTitle: "" }
        ]
    }
}

const _artist = (state, action) => {
    switch (action.type) {
        case SET_ARTIST_ACTION:
            return action.artist
        case SET_ARTIST_NAME_ACTION:
            return {...state, artistName: action.artistName}
        default:
            return state
    }
}

export const getArtist = state =>
    state.artist

const artist = (state = initialState, action) => {
    return {
        artist: _artist(state.artist, action)
    }
}

export default artist

/album-search-app/src/reducers/album.jsx

/album-search-app/src/reducers/album.jsx
import {SET_ALBUM_ACTION} from '../constants/ActionTypes';

const initialState = {
    /*
    album: {
        productTitle: "",
        imgUrl: "",
        productUrl: "",
        artistName: "",
        artistPageUrl: "",
        pageNo: 1,
        songList: [
            //{ songName: ""}
        ]
    }
    */
    album: {
        albumId: 0,
        artistId: 0,
        artistName: "",
        albumPageNo: 0,
        artistPageUrl: "",
        imgUrl: "",
        productTitle: "",
        productUrl: "",
        songIdList: [
            //{albumId: 1, songOrderNo: 1, songId: 1}
        ],
        songList: [
            //{songId: 1, songName: 'GO', albumId: '1', songOrderNo: 0, songUrl: '/song/1', …}
            //{albumId: "1", artistId: 24, songId: 1, songName: "GO", songOrderNo: 0, songUrl: "/song/1", videoUrl: "https://aaaa111.xxx"}
        ]
    }
}

const _album = (state, action) => {
    switch (action.type) {
        case SET_ALBUM_ACTION:
            return action.album
        default:
            return state
    }
}

export const getAlbum = state =>
    state.album

const album = (state = initialState, action) => {
    return {
        album: _album(state.album, action)
    }
}

export default album

●ストアの作成

ReduxのcreateStoreが非推奨になった件
https://qiita.com/pochy9n/items/1da42676ad2b36a1ada7

createStoreが非推奨になったのは、Redux 4.2.0からだった。
createStoreは「非推奨(deprecated)」であって「廃止(obsolete)」ではないので、まだ使えるようだが、このような警告を放置して先に進むと、ろくなことが起きないので対処することにした。

具体的には、VS Codeのメッセージにしたがって、以下のように修正した。

npm install @reduxjs/toolkit

でRedux Toolkitパッケージを追加

import { createStore } from 'redux';

import { configureStore } from '@reduxjs/toolkit';

に変更

createStore(rootReducer)

configureStore({ reducer: rootReducer })

に変更
これで取り敢えずは警告が出なくなった。
ただし、まだ、途中なので、この先に何が起きるか分からない。

/album-search-app/src/store.jsx

/src/store.jsx
import { configureStore } from '@reduxjs/toolkit'; 
import reducer from './reducers';

const store = configureStore(
    {reducer: reducer}
);

//export const history = createBrowserHistory();

export default store

/src/history.jsx

/src/history.jsx
import { createBrowserHistory } from 'history';

export default createBrowserHistory();

必要なパッケージ随時追加

npm install redux-logger
npm install history
npm install react-router-redux

npm install prop-types

■サーバーからJSONデータをリクエストする(main.jsx参照)と次のエラーがでるかもしれない

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
ブラウザ側でエラーが出る
Access to fetch at 'http://127.0.0.1:3000/artistlist' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

[album-search-app-apiの話]
■ExpressでCORSエラーが起きたらcorsで解決しよう
https://zenn.dev/luvmini511/articles/d8b2322e95ff40
まずCORSについてからです。

CORSは「Cross-origin resource sharing」の略です。
正確な定義はMDNの説明を見てみましょう。

追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、
異なるオリジンにある選択されたリソースへのアクセス権を与えるよう
ブラウザーに指示するための仕組み

ウェブの世界では違うオリジン同士のリソース共有に関するポリシーが2つあります。
1つはつい先説明したCORS、もう1つはSOPです。

SOPはSame-Origin Policyの略で、文字通りに同じオリジン同士だけリソース共有ができるというポリシーです。

しかし、ウェブ世界で違うオリジンからリソースを持ってくることは頻繁に起こること
(外部API使用など)なので完全に制限することは難しいです。
ですのでいくつかの例外をおいて、その例外に該当する場合は違うオリジンでもアクセスを許可しています。

その例外の中で1つがCORSポリシーを守るリソースリクエストです。

違うオリジンにリソースリクエストを送ったらSOP違反になるし、例外のCORSポリシーも守らなかったら違うオリジンからのリソースは使えなくなるということです。

〇エラーの原因
さて、最初にお見せしたエラーの原因を話しましょう。
これはブラウザからバックエンドサーバーにリクエストを送ったときに起こったエラーです。
内容をよく見たら「http://localhost:3000からhttp://localhost:3065/userへ送ったアクセス
がCORSポリシーによってブロックされた」と書いてます。

クライアント側のポート番号が3000でバックエンド側のポート番号が3065なので違うオリジンですね。
ここで既にSOP違反してるのにCORSポリシーを守る処理も入れてないからブロックされちゃったというシナリオでしょう。

!!!!!!!!!!![album-search-app-apiの話]
■解決: ミドルウェア
expressならミドルウェアでCORS設定したほうが楽だと思います。
そのミドルウェアの名前もcorsです(紛らわしい)。

npm install cors

D:\proj\JavaScript\node_js\album-search-app-api>npm install cors

コマンドで簡単にインストールできます。

!!!!!!!!!!![album-search-app-apiの話]
/album-search-app-api/app.jsで以下のように実装した

/album-search-app-api/app.js
//(略)
const app = express();
////////////
const cors = require('cors');

//////////////
app.use(cors());
/*
app.use(cors({
    origin: 'http://localhost:5173', //アクセス許可するオリジン
    credentials: true, //レスポンスヘッダーにAccess-Control-Allow-Credentials追加
    optionsSuccessStatus: 200 //レスポンスstatusを200に設定
}));
*/
////////////

成功した。 ポート5137(React)からサーバーポート3000(Express)のデータリクエストができた。

!!!!!!!!!!![album-search-app-apiの話]
■NodeでWebアプリ】(3) Sequelizeでデータベース接続
https://blog.otasys.co.jp/2019/07/03/express-3/#:~:text=%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%82%80&text=var%20db%20=%20require('../models/',%E3%81%AE%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82&text=%7D-,%7D);,%E7%B0%A1%E5%8D%98%E3%81%AB%E8%AA%AC%E6%98%8E%E3%81%97%E3%81%BE%E3%81%99%E3%80%82&text=%E3%81%93%E3%82%8C%E3%81%AF%E3%80%81/user/json,%E3%81%AE%E3%83%AC%E3%83%83%E3%82%B9%E3%83%B3%E3%81%A7%E3%82%82%E5%AD%A6%E3%81%B3%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82&text=%E3%81%93%E3%81%93%E3%81%A7%E3%81%AF%E3%80%81Sequlize%E3%81%AE%20findAll,%E3%81%97%E3%81%9F%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%8F%97%E3%81%91%E5%8F%96%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82&text=%E3%81%93%E3%81%93%E3%81%A7%E3%81%AF%E3%80%81%20.,%E3%82%A8%E3%83%A9%E3%83%BC%E3%82%92%E8%BF%94%E3%81%97%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%80%82&text=12:%20res.json(users,%E3%81%AE%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82

データベースのデータを読み込む
先ずデータを読み込む部分ですが、これは次の1行を書くだけです。

var db = require('../models/');

次に、データを出力する部分は以下のようになります。

// ユーザーのリストをJSON出力する
router.get('/json/', function(req, res, next) {

    // Sequelizeのモデルを使ってデータを取得する
    db.User.findAll().then(users => {

        if (!users) {
            console.log("ユーザーデータを取得できませんでした");
            res.send('Error');
        } else {
            res.json(users);
        }
    });
});

■ awaitを使った場合

すべてのカラムを対象にする
何も引数に指定しない場合、すべてのカラムが抽出の対象となります。

const rows = await User.findAll()

この場合でもSQLでは*ではなくすべてのカラムが指定されるようです。

SELECT id, name, age FROM Users AS User;

npm install react-router-dom

〇〇〇〇〇〇〇〇[album-search-appの話に戻る]
■キャッシュの無効化
Next.jsの4つのキャッシュメカニズムについて
https://zenn.dev/wac/articles/7cc49325c66c71

Opting out(除外、無効化)
個々のデータのfetchでは、cacheオプションをno-storeに設定することで、キャッシュを無効ににできます。
これは、fetchが呼び出されるたびにデータが取得されることを意味します。

// Opt out of caching for an individual `fetch` request
fetch(`https://...`, { cache: 'no-store' })

/album-search-app/src/main.jsx

/album-search-app/src/main.jsx
/*略*/
        console.log("fetch: url=" + url);
        //
        //const response = await fetch(url);
        // 個々のデータのfetchでは、cacheオプションをno-storeに設定することで、
        // キャッシュを無効ににできます。
        const response = await fetch(url, { cache: 'no-store' });

        const data = await response.json();

■各ページのレンダリング

〇インデックスページ(/)
image.png

/album-search-app/src/indexPage.jsx

/album-search-app/src/indexPage.jsx
import { useNavigate  } from 'react-router-dom' // React Routerを使うためのもの
import SearchPageContainer from './containers/searchPageContainer'

function indexPage(props) {
    const history = props.history;

    //console.log("props.history:");
    //console.log(history);

    return (
        <>
            <SearchPageContainer history={history} />
        </>
    )
}

export default indexPage

/album-search-app/src/containers/searchPageContainer.jsx

/album-search-app/src/containers/searchPageContainer.jsx
import React from 'react'
//import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {getArtist} from '../reducers'
import {setArtistName} from '../actions'
import SearchForm from '../components/searchForm'

function searchPageContainer(props) {
    const history = props.history;
    const artist = props.artist;
    const setArtistName = props.setArtistName;

    //console.log("artist:");
    //console.log(artist);

    return (
        <>
            <h1>アーティスト検索</h1>
            <SearchForm
                history={history}
                artist={artist}
                setArtistName={setArtistName}
            />
        </>
    )
}

const mapStateToProps = state => ({
    artist: getArtist(state)
})

export default connect(
    mapStateToProps,
    {setArtistName}
)(searchPageContainer)

/album-search-app/src/constants/constants.jsx

/album-search-app/src/constants/constants.jsx
export const ARTIST_LIST_PAGE_URL = "/artistlist";
export const ARTIST_PAGE_URL = "/artist";
export const ALBUM_PAGE_URL = "/album";

/album-search-app/src/constants/ActionTypes.jsx

/album-search-app/src/constants/ActionTypes.jsx
export const SET_ARTIST_LIST_ACTION = 'SET_ARTIST_LIST_ACTION'
//////////////////////////////////////
export const SET_ARTIST_ACTION = 'SET_ARTIST_ACTION'
export const SET_ARTIST_NAME_ACTION = 'SET_ARTIST_NAME_ACTION'
export const SET_ALBUM_ACTION = 'SET_ALBUM_ACTION'

/album-search-app/src/components/searchForm.jsx

/album-search-app/src/components/searchForm.jsx
import React from 'react'
import PropTypes from 'prop-types'
//import { useNavigate } from "react-router-dom";

import * as constants from '../constants/constants';

function searchForm(props) {
    //const navigate = useNavigate();
    let state = {};

    function setState(value) {
        state = value;
    }

    // アーティスト名テキストが変更された
    function artistNameText_onChange(e) {
        //setState({...this.myState, artistName: e.target.value});
        setState({...state, artistName: e.target.value});

        console.log("onChange state:");
        console.log(state);
    }

    // 検索フォームが送信された
    function searchForm_onSubmit({e, setArtistName, artistListUrl, history}) {
        e.preventDefault();
        console.log("searchForm_onSubmit state:");
        console.log(state);

        const artistName = state.artistName;
        setArtistName(artistName);

        const artistListPageUrl = constants.ARTIST_LIST_PAGE_URL;
        //const listPageUrlByName = artistListPageUrl + '/' + artistName;
        const listPageUrlByName = artistListPageUrl + '?key=' + artistName;
        console.log("listPageUrlByName:" + listPageUrlByName);
        window.location.href = listPageUrlByName;
    }

/////////////////////////////////////////////////////////////// 処理    /////////////////////////////////////////////////////////////
    const {artistListUrl, artist, setArtistName, history} = props;
    let artistName = artist.artistName;
    //state = {artistName: artistName};
    setState({artistName: artistName});
    
    //console.log("artistName in searchForm):" + artistName);
    //console.log("artist in searchForm:");
    //console.log(artist);
    
    return (
        <>
            <form action="" method="post" className="searchContainer" onSubmit={
                e => searchForm_onSubmit({e, setArtistName, artistListUrl, history})
            }>
                <p>アーティスト名で検索</p>
                <input id="artistNameText" type="text" name="artist_name"
                    defaultValue={artistName}
                    onChange={artistNameText_onChange.bind(this)}/>
                <input type="submit" className="searchBtn" value="検索" />
            </form>
        </>
        )
    }

searchForm.propTypes = {
    artistListUrl: PropTypes.string.isRequired,
    history: PropTypes.object.isRequired,
    artist: PropTypes.object.isRequired,
    setArtistName: PropTypes.func.isRequired
}

export default searchForm

〇アーティスト検索結果ページ(/artistlist?key=keyWord)
image.png

/album-search-app/src/artistListPage.jsx

/album-search-app/src/artistListPage.jsx
//import { useNavigate  } from 'react-router-dom' // React Routerを使うためのもの
import { Link } from 'react-router-dom';

import {connect} from 'react-redux';
import {getArtistList} from './reducers';
//import {setArtistList} from './actions';

function artistListPage(props) {
     /////////////////////////////////////////////////////////////
// 処理
////////////////////////////////////////////////////////////
    const artistList = props.artistList;
    const history = props.history;

    console.log("artistList:");
    console.log(artistList);

    return (
        <>
        <h1>アーティスト検索結果</h1>
        <ul>
            {artistList.map((data, index) => (
                <li key={index}>{index}:<Link to={data.artistPageUrl}>{data.artistName}</Link></li>
            ))}
        </ul>

        </>
    )

}
const mapStateToProps = state => ({
    artistList: getArtistList(state)
})

export default connect(
    mapStateToProps,
)(artistListPage)

〇アーティストページ(/artist/:artistId)
image.png

/album-search-app/src/artistPage.jsx

/album-search-app/src/artistPage.jsx
import { Link, useParams } from "react-router-dom";

import {connect} from 'react-redux';
import {getArtist} from './reducers';

function artistPage(props) {
    const params = useParams();

     ////////////////////////////////////////////////////////////
// 処理
////////////////////////////////////////////////////////////
    const artist = props.artist;
    const history = props.history;

    const artistId = params.artistId;
    console.log("artistId:" + artistId);
    console.log("artist:");
    console.log(artist);
    const artistName = artist.artistName;
    const artistImgUrl = artist.artistImgUrl;
    const albumList = artist.albumList;

    return (
        <>
        <h1>{artistName}(ID:{artistId})</h1>
        <img src= {artistImgUrl}/>
        <ul>
            {albumList.map((data, index) => (
                <li key={index}>{index}:{data.productTitle}</li>
            ))}
        </ul>

        <ul>
            {albumList.map((data, index) => (
                <li key={index}>
                    {index}:<Link to={data.detailPageUrl}>{data.productTitle}</Link>
                </li>
            ))}
        </ul>
        </>
    )

}

const mapStateToProps = state => ({
    artist: getArtist(state)
})

export default connect(
    mapStateToProps,
)(artistPage)

〇アルバムページ(/album/:albumId)
image.png

/album-search-app/src/albumPage.jsx

import { Link, useParams } from "react-router-dom";

import {connect} from 'react-redux';
import {getAlbum} from './reducers';

function albumPage(props) {
    const params = useParams();

     ////////////////////////////////////////////////////////////
// 処理
////////////////////////////////////////////////////////////
    const album = props.album;
    const history = props.history;

    const albumId = params.albumId;
    console.log("albumId:" + albumId);
    console.log("album:");
    console.log(album);
    const artistName = album.artistName;
    const artistPageUrl = album.artistPageUrl;
    const albumImgUrl = album.imgUrl;
    const albumTitle = album.productTitle;
    const productUrl = album.productUrl;
    const songList = album.songList;

    return (
        <>
        <h1>{albumTitle}(ID:{albumId})</h1>
        <img src= {albumImgUrl}/>
        <ul>
            {songList.map((data, index) => (
                <li key={index}>{index}:{data.songName}</li>
            ))}
        </ul>

        <ul>
            {songList.map((data, index) => (
                <li key={index}>
                    {index}:<Link to={/*data.videoUrl*/data.songUrl}>{data.songName}</Link>
                </li>
            ))}
        </ul>
        </>
    )

}

const mapStateToProps = state => ({
    album: getAlbum(state)
})

export default connect(
    mapStateToProps,
)(albumPage)

//////////////////////////////////////
■実行
ターミナルに

npm run dev

を打ち、「Enter」で実行する

http://localhost:5173 を開いてみましょう。

■「Vite + React」のbuild方法

開発が完了し、オンラインで公開するときには「build」という作業を行います

npm run build

distという新しいフォルダが生成されています。

#4. まとめ

アルバム検索アプリをnode js(Docker Express & React + Redux)のプロトタイプを実装しました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?