とあるWeb屋の備忘録

とあるWeb屋の備忘録。たまに雑記。

配列操作でよく使うメソッドまとめ03

前回の配列操作でよく使うメソッドまとめ02つづき。
今回も前回までと同じJSONデータを使ってfilterの使い方についてまとめます。
JSONデータの中身は配列操作でよく使うメソッドまとめ01のページ下部に載っています。

各チームのUS出身の選手のみ取り出す

import data from '../EasternConference.json'
let menberArry = data.map(v => v["menbers"])
let countryFilterArry = []
for(let i = 0; i < menberArry.length; i++) {
    countryFilterArry.push(menberArry[i].filter(el => el.from == "US"))
}
console.log(countryFilterArry) // フィルタリングした結果
console.log(menberArry) // フィルタリングする前の配列のまま
return countryFilterArry

まずフィルタリングした結果を格納するための配列を準備しておきます。

let countryFilterArry = []

for文の中でfilter関数を実行しています。filter関数の引数には各チームのメンバーオブジェクトが入ります。
filter関数はfromがUSと一致するメンバーオブジェクトのみを返すので、それを最初に準備しておいたcountryFilterArryに格納して最後にreturnすれば完了です。

ソース内だと以下の部分

for(let i = 0; i < menberArry.length; i++) {
    countryFilterArry.push(menberArry[i].filter(el => el.from == "US"))
}
console.log(countryFilterArry) // フィルタリングした結果
console.log(menberArry) // フィルタリングする前の配列のまま
return countryFilterArry

filterメソッドは破壊的なメソッドではないので元の配列のデータには影響を及ぼしません。

参考資料
Array.prototype.filter()

配列操作でよく使うメソッドまとめ02

前回の配列操作でよく使うメソッドまとめ01つづき。
今回は前回のJSONデータを使ってsortの使い方についてまとめます。
JSONデータの中身は前回のページ下部に記載しています。

各チームの選手を年齢順にソート

import data from '../EasternConference.json'
let menberArry = data.map(v => v["menbers"])
for(let i =0; i < menberArry.length; i++) {
  menberArry[i].sort((a, b) => a.age - b.age)
  console.log(menberArry[i])
}

for文の中でsort関数を実行しています。sort関数は引数に比較関数を渡してその結果に応じて昇順/降順が決まります。
ソース内だと以下の部分

sort((a, b) => a.age - b.age)

これは単純にオブジェクトのキーを指定して、引き算をしているだけです。この引き算の戻り値が正か負かを気にすれば良いだけです。
戻り値が負なら昇順、正なら降順になります。
今回は昇順で良いのでこの順番ですが、降順にしたければ以下のようにすれば良いです。

sort((a, b) => b.age - a.age)

名前の長さ順なら以下

sort((a, b) => a.name.length - b.name.length)

ちなみにsortは破壊的メソッドになるため元データの並び順が変わります。
元データを壊さずソートした配列を使いたい場合は以下のようにsliceを使って元のデータのコピーをソートすれば良いです。
sliceで配列をコピーするのでコピー用の配列sliceArryを作ってそれにpushしていきます。

let menberArry = data.map(v => v["menbers"])
let sliceArry = []
for(let i =0; i < menberArry.length; i++) {
  sliceArry.push(menberArry[i].slice().sort((a, b) => a.name.length - b.name.length))
}

これでmenberArryを壊すことなくsortした結果をsliceArryに格納することができました。
また、sliceを使う方法はシャローコピーになるためsliceArryのデータを変更すると元データも影響を 受けます。
それを避けたい場合はディープコピーして元データに影響がないようにする必要がありますので、シャローコピーされていることを頭の片隅に置いておくと良いかもしれません。

参考資料
Array.prototype.sort()

配列操作でよく使うメソッドまとめ01

仕事で配列を扱うことが多くなったので配列の操作方法をまとめました。
現場ではAPIで取得したデータをフロント側で扱いやすいようにJSONにパースしたり加工することが多いと思うので、今回は基本的な計算処理についてまとめます。
今回は実際にNBAの複数のチーム全体の平均年齢を算出、各チームの平均年齢を算出の2点を実際に行います。

ページ下部に今回使っているJSONデータを載せておきます。

JSONをインポートさせて実際にソース書いていきます。

import data from '../EasternConference.json'

複数のチーム全体の平均年齢を算出

let ageArry = data.map((v) => v["menbers"]).reduce((a, b) => a.concat(b), [])
let result = Math.round(ageArry.map((v) => v["age"]).reduce((a,b) => a + b, 0) / ageArry.length)
return result // 26

まずmapでmenberのみの配列を作って(二次元配列になるので注意)、扱いやすいようにreduceで二次元配列を一次元配列に戻します。
一次元配列に戻した配列を今度はageのみの配列にして、reduceで足しあげて平均を出せば完了です。

各チームの平均年齢を算出

let ageArry = data.map(v => v["menbers"])
let perAgeArry = []
for(let i = 0; i < ageArry.length; i++ ) {
  perAgeArry.push({...data[i], "ageAverage": Math.round(ageArry[i].map(v => v["age"]).reduce((a, b) => a + b, 0) / ageArry[i].length)})
}
return perAgeArry //  // 元のJSONに"ageAverage": n を追加した配列

全体の平均を求めるときと違うのは、スプレッド構文を使って元のJSONデータに「チームごとの平均年齢」を追加していることです。
そのために新しい配列をひとつ準備しておき、for文で回す際にその配列にチームの平均年齢をpushしていきます。
最後にその配列を返せば完了です。

一番下にJSON載せておきます。(今回これを用意するのに一番時間かかりました...国コードも載せてあるので出身地ごとに割合出したりとか後でやれたらと思います)

参考資料

Array.prototype.map()
Array.prototype.reduce()

[
  {
    "id": 1,
    "team": "Boston Celtics",
    "menbers": [
      {
        "name": "Kadeem Allen",
        "age": 25,
        "from": "US"
      },
      {
        "name": "Aron Baynes",
        "age": 32,
        "from": "AU"
      },
      {
        "name": "Jabari Bird",
        "age": 24,
        "from": "US"
      },
      {
        "name": "Jaylen Brown",
        "age": 22,
        "from": "US"
      },
      {
        "name": "Jonathan Gibson",
        "age": 31,
        "from": "US"
      },
      {
        "name": "Gordon Hayward",
        "age": 28,
        "from": "US"
      },
      {
        "name": "Al Horford",
        "age": 32,
        "from": "DM"
      },
      {
        "name": "Kyrie Irving",
        "age": 26,
        "from": "US"
      },
      {
        "name": "Shane Larkin",
        "age": 26,
        "from": "US"
      },
      {
        "name": "Marcus Morris",
        "age": 29,
        "from": "US"
      },
      {
        "name": "Semi Ojeleye",
        "age": 24,
        "from": "US"
      },
      {
        "name": "Terry Rozier",
        "age": 24,
        "from": "US"
      },
      {
        "name": "Xavier Silas",
        "age": 30,
        "from": "US"
      },
      {
        "name": "Marcus Smart",
        "age": 24,
        "from": "US"
      },
      {
        "name": "Jayson Tatum",
        "age": 20,
        "from": "US"
      },
      {
        "name": "Daniel Theis",
        "age": 26,
        "from": "DE"
      },
      {
        "name": "Guerschon Yabusele",
        "age": 23,
        "from": "FR"
      }
    ]
  },
  {
    "id": 2,
    "team": "Brooklyn Nets",
    "menbers": [
      {
        "name": "Quincy Acy",
        "age": 28,
        "from": "US"
      },
      {
        "name": "Jarrett Allen",
        "age": 20,
        "from": "US"
      },
      {
        "name": "DeMarre Carroll",
        "age": 32,
        "from": "US"
      },
      {
        "name": "Allen Crabbe",
        "age": 26,
        "from": "US"
      },
      {
        "name": "Spencer Dinwiddie",
        "age": 25,
        "from": "US"
      },
      {
        "name": "Milton Doyle",
        "age": 25,
        "from": "GH"
      },
      {
        "name": "Jared Dudley",
        "age": 33,
        "from": "US"
      },
      {
        "name": "Kenneth Faried",
        "age": 29,
        "from": "US"
      },
      {
        "name": "Joe Harris",
        "age": 27,
        "from": "US"
      },
      {
        "name": "Rondae Hollis-Jefferson",
        "age": 23,
        "from": "US"
      },
      {
        "name": "Caris LeVert",
        "age": 24,
        "from": "US"
      },
      {
        "name": "Timofey Mozgov",
        "age": 32,
        "from": "RU"
      },
      {
        "name": "Jahlil Okafor",
        "age": 23,
        "from": "US"
      },
      {
        "name": "D'Angelo Russell",
        "age": 22,
        "from": "US"
      },
      {
        "name": "Nik Stauskas",
        "age": 25,
        "from": "CA"
      },
      {
        "name": "James Webb III",
        "age": 25,
        "from": "US"
      },
      {
        "name": "Isaiah Whitehead",
        "age": 23,
        "from": "US"
      }
    ]
  },
  {
    "id": 3,
    "team": "New York Knicks",
    "menbers": [
      {
        "name": "Ron Baker",
        "age": 25,
        "from": "US"
      },
      {
        "name": "Trey Burke",
        "age": 26,
        "from": "US"
      },
      {
        "name": "Damyean Dotson",
        "age": 24,
        "from": "US"
      },
      {
        "name": "Tim Hardaway, Jr.",
        "age": 26,
        "from": "US"
      },
      {
        "name": "Isaiah Hicks",
        "age": 24,
        "from": "US"
      },
      {
        "name": "Jarrett Jack",
        "age": 35,
        "from": "US"
      },
      {
        "name": "Enes Kanter",
        "age": 26,
        "from": "TR"
      },
      {
        "name": "Luke Kornet",
        "age": 23,
        "from": "US"
      },
      {
        "name": "Courtney Lee",
        "age": 33,
        "from": "US"
      },
      {
        "name": "Emmanuel Mudiay",
        "age": 22,
        "from": "CD"
      },
      {
        "name": "Frank Ntilikina",
        "age": 20,
        "from": "FR"
      },
      {
        "name": "Kyle O'Quinn",
        "age": 28,
        "from": "US"
      },
      {
        "name": "Kristaps Porziņģis",
        "age": 23,
        "from": "LV"
      },
      {
        "name": "D'Angelo Russell",
        "age": 22,
        "from": "US"
      },
      {
        "name": "Lance Thomas",
        "age": 30,
        "from": "US"
      },
      {
        "name": "Noah Vonleh",
        "age": 23,
        "from": "US"
      },
      {
        "name": "Troy Willams",
        "age": 24,
        "from": "US"
      }
    ]
  },
  {
    "id": 4,
    "team": "Philadelphia 76ers",
    "menbers": [
      {
        "name": "Jimmy Butler",
        "age": 29,
        "from": "US"
      },
      {
        "name": "Wilson Chandler",
        "age": 31,
        "from": "US"
      },
      {
        "name": "Joel Embiid",
        "age": 24,
        "from": "CM"
      },
      {
        "name": "Markelle Fultz",
        "age": 20,
        "from": "US"
      },
      {
        "name": "Amir Johnson",
        "age": 31,
        "from": "US"
      },
      {
        "name": "Furkan Korkmaz",
        "age": 21,
        "from": "TR"
      },
      {
        "name": "Mike Muscala",
        "age": 27,
        "from": "US"
      },
      {
        "name": "T. J. McConnell",
        "age": 26,
        "from": "US"
      },
      {
        "name": "Justin Patton",
        "age": 21,
        "from": "US"
      },
      {
        "name": "Jacob Pullen",
        "age": 29,
        "from": "US"
      },
      {
        "name": "J. J. Redick",
        "age": 34,
        "from": "US"
      },
      {
        "name": "Ben Simmons",
        "age": 22,
        "from": "AU"
      }
    ]
  },
  {
    "id": 5,
    "team": "Toronto Raptors",
    "menbers": [
      {
        "name": "OG Anunoby",
        "age": 21,
        "from": "GB"
      },
      {
        "name": "Lorenzo Brown",
        "age": 28,
        "from": "US"
      },
      {
        "name": "Danny Green",
        "age": 31,
        "from": "US"
      },
      {
        "name": "Serge Ibaka",
        "age": 29,
        "from": "ES"
      },
      {
        "name": "Kawhi Leonard",
        "age": 27,
        "from": "US"
      },
      {
        "name": "Kyle Lowry",
        "age": 32,
        "from": "US"
      },
      {
        "name": "Alfonzo McKinnie",
        "age": 26,
        "from": "US"
      },
      {
        "name": "C. J. Miles",
        "age": 31,
        "from": "US"
      },
      {
        "name": "Malcolm Miller",
        "age": 25,
        "from": "US"
      },
      {
        "name": "Greg Monroe",
        "age": 28,
        "from": "US"
      },
      {
        "name": "Norman Powell",
        "age": 25,
        "from": "US"
      },
      {
        "name": "Malachi Richardson",
        "age": 22,
        "from": "US"
      },
      {
        "name": "Pascal Siakam",
        "age": 24,
        "from": "CM"
      },
      {
        "name": "Jonas Valančiūnas",
        "age": 26,
        "from": "LT"
      },
      {
        "name": "Fred VanVleet",
        "age": 24,
        "from": "US"
      },
      {
        "name": "Delon Wright",
        "age": 26,
        "from": "US"
      }
    ]
  }
]

HTTP通信のこと

今までよくわかっていなかったHTTP通信周りの知識を得るためにこちらの本を読みました。
初版が2010年なのでほんの一部だけ古い内容などもありますが、全体を通してすごく面白くて一気読みできましたし、とても勉強になりました。

よくわからないところは流し読みしてだいたい5時間程度で読めた感じです。
自分が知りたかった内容が網羅されていたので個人的にはとても満足できる一冊でした。

こちらの本で学習したことを備忘録として簡単にまとめていきます。
※分かりやすく説明するために簡単にまとめていますので、誤解を招く表現などありましたらご指摘いただけると嬉しいです!!


URIってなに?

  • Web上にあるリソースを一意に識別するためのIDのこと
    • Uniform Resource Identifierの略

URIの構成

  • スキーム + ホスト名 + パス + クエリ
    • この構成は代表的な構成で、ほかにも色々な構成がある
/*
スキーム http://
ホスト名   news.google.com
パス     /
クエリ   ?hl=ja&gl=JP&ceid=JP:ja
*/
https://news.google.com/?hl=ja&gl=JP&ceid=JP:ja



HTTPってなに?

  • Web上でリソースのやりとりをするときに使うプロトコル
    • これを使えばHTML、XML、画像、Javascript、各種オフィス、PDFなど何でも転送できる
  • TCP/IPベースのプロトコル
    • TCP/IPについてはこのあとに書く
  • アーキテクチャがクライアントサーバ
    • クライアントからリクエストを投げる→サーバがそれを受けてレスポンスを返す
  • ステートレス
    • TCP/IPの説明のときに書く

TCP/IPとは

ネットワーク層(ここで使用されるプロトコルはIP)でパケットを送信する。IPを使って宛先を特定する。
トランスポート層(ここで使用されるプロトコルTCPまたはUDP)でコネクション張る。(ネットワーク上の接線)

コネクションとセッションを混同しないための以下を覚えておく。

  • コネクションはネットワークレイヤーの話でセッションはWebの話。
  • ネットワークレイヤーの上位階層がWebのレイヤーになる。

つまりHTTPとセッションは関係ない(=HTTPにはセッションの概念がない)
ここでステートレスの話とつながってくる。
そもそもステートレスが何かというと「アプリケーションの状態を持たないこと(=セッション状態を持たないこと)」
セッションについて分かりやすく言うとECサイトでログインして買い物してログアウトするまでが1セッション。
ログイン~ログアウトまでが1セッションなので、この一連の流れ(クライアントが何をリクエストしたかという情報)を持たないってこと。
この間の「ログイン情報」とか「買い物かごの中身」を保持するためにECサイトECサイトのサーバ)がセッションIDを作ってCookieとしてHTTPリクエストに含める。
これによりHTTPはステートレスでも「状態」を保持したリクエストをサーバに送れるので問題なくクライアントサーバでやり取りできる。
このおかげでクライアントが「情報を保持してリクエスト」できるし、サーバは「ステートレスなのでスケールが簡単に行える」。(=クライアントが情報を保持しているため、どのサーバにリクエストが送られても関係なく対応できる)

クライアントサーバとは

HTTPメッセージ

リクエストとレスポンスはHTTPメッセージと総称される。

HTTPメッセージ(リクエスト)の構成

  • リクエストライン
  • ヘッダ
  • ボディ

HTTPメッセージ(レスポンス)の構成

  • ステータスライン
  • ヘッダ
  • ボディ

後日HTTPメッセージのヘッダについてまとめようと思います!
ここまで読んでいただいてありがとうございました!!!

Promise基礎

仕事でNode.jsを使うことになって非同期処理でPromiseやasync/awaitをよく使うので今回はPromiseについてまとめました。
async/awaitについては別の機会にまとめようと思います。


Promiseの特徴は4つあります。

  • Promiseオブジェクトは3つの状態を持っている(Pending, fulfilled, rejected)
  • 状態は変化する(Pending -> fulfilled or rejected)
  • Promiseの状態によってその後の処理が変化する
  • then()で処理をチェーンできる(callback地獄にならない)

Promiseの使い方

  • return new Promise((resolve, reject) => {do something})でPromiseオブジェクトを作ってreturnさせる関数を用意する(作ったときは状態はPending)
  • その関数を呼び出して状態変化させたPromiseオブジェクトを次の処理に渡す
  • 次の処理ではPromiseオブジェクトの状態によって処理が分岐する(以下のソースではconsole.log(result)orconsole.log(error)

※Promiseの使い方補足
Promiseオブジェクトはresolveの処理が行われるとfulfilledになる
Promiseオブジェクトはrejectの処理が行われるとrejectedになる
以下のソースのpmFnc(true)が呼ばれるとPromiseオブジェクトはresolveを実行してfulfilledになる
Promise(pmFnc(true))の結果はコールバック関数の引数(.then(result => ...)で受け取れる(resultのこと)

function pmFnc(result) {
  return new Promise ((resolve, reject) => {

    if(result) resolve('success!')
    reject('failure...')
  })
}

pmFnc(true)
.then(result => {
  console.log(result)
})
.catch( error => {
  console.log(error)
})

console.log('1st comment')

結果は以下

1st comment
success!


簡単にですがPromiseについてまとめました!
次はasync/awaitについてまとめたいと思います。

読んでいただいてありがとうございました!!

Express基礎

復習しながら自分用にmdでメモってましたがせっかくなので備忘録としてこちらに残します。
2018.11月からB2BSaaSに転職したことでNode.jsを書く機会があるのでNodeの学習を再開しました。
前に学習していたときから時間が空いて色々忘れている部分も多かったのでexpress-generatorの雛形+過去に自分が書いた記事を見て復習中です。

Node.jsについて書いた過去のエントリはこちら



Express自体

ルーティングとミドルウェアのWebフレームワーク
Expressはリクエストが来たら上から評価される

Expressアプリケーション

一連のミドルウェア関数呼び出し
(連続してミドルウェア関数を呼び出すことで成り立っている)

ミドルウェア関数

以下の3つ所有

  • requestオブジェクト
  • responseオブジェクト
  • request-responseサイクルにおける次のミドルウェア関数に対するアクセス権
    次のミドルウェア関数nextという変数名であらわされる

実行できる4つのタスク

  • 任意のコードを実行
  • requestオブジェクト、responseオブジェクトを変更
  • request-responseサイクルを終了する
  • スタック内の次のミドルウェアを呼び出す
    ※現在のミドルウェア関数がrequest-responseサイクルを終了しない場合はnext()を使って次のミドルウェア関数に制御を渡さなければならない。そうしないとrequestはハングしたままになりガーベジコレクションの対象にならない

Expressアプリケーションで使用できる4タイプのミドルウェア


アプリケーション・レベルのミドルウェアについて

以下の例はapp.use()を使って/ミドルウェアをマウントしている
* /ミドルウェアのサブスタックが形成されたことになる
* /のパスは設定しなくてもよくて、設定しない場合はアプリケーションがリクエストを受け取るたびに実行される
* next('route')を使うことで次のルートに制御を渡している
※app.use()だとnext('route')使えないので注意する(app.METHOD()もしくはrouter.METHOD()で使用可能)

/* GET home page. */
app.get('/', function (req, res, next) {
  if (req.params.id === undefined) {
    next('route')
  }else {
    next()
  }
}, function (req, res, next) {
  res.render('index', { title: 'Express02' });
})

app.get('/', function (req, res, next) {
  res.render('index', { title: 'Express' });
})

エラー処理ミドルウェアについて

  • 引数は(err, req, res, next)の4つでなければいけない
  • 他のapp.use()とroutes呼び出し後に定義する(最後に定義するということ)
  • エラーハンドリング用にapp.use()を使ってエラー処理関数を定義する
  • エラー処理関数でnextを呼ばないときはレスポンスを記述する必要がある
    next(err)を呼び出した時点で、エラー処理関数だけ実行され他のミドルウェア関数は実行されない


エラーを発生させる

var createError = require('http-errors');を使ってエラーを発生させる

エラーコードは以下ページ参照

https://www.npmjs.com/package/http-errors

app.use('/', function(req, res, next) {
  next(createError(404));
});

app.use(function(err, req, res, next) {
  console.log(err) // NotFoundError: Not Found
  console.log(err.message) // Not Found
  console.log(err.status) // 404

  // request-responseサイクルのローカル変数(res.locals)はクライアントにレスポンスを返すまで参照することができる
  // req.app.get('env')で環境を検出
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  //レスポンスのステータスコードを指定
  res.status(err.status || 500);
  res.render('error');
});


また、express-generatorの雛形のapp.jsのソースを色々調べて備忘録コメント残したものが以下です。

// Create HTTP errors for Express, Koa, Connect, etc. with ease.
var createError = require('http-errors');
// The path module provides utilities for working with file and directory paths
var path = require('path');

var express = require('express'); 
var cookieParser = require('cookie-parser'); // サード・パーティー・ミドルウェア
var logger = require('morgan'); // HTTP request logger middleware for node.js. サード・パーティー・ミドルウェア

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
console.log(__dirname) // D:\localhost\node\www

app.use(logger('dev')); // ログの出力フォーマットを指定
app.use(express.json()); // This is a built-in middleware function in Express. It parses incoming requests with JSON payloads and is based on body-parser.JSONペイロードで受信したリクエストを解析する
app.use(express.urlencoded({ extended: false })); // This is a built-in middleware function in Express. It parses incoming requests with urlencoded payloads and is based on body-parser.URLエンコードされたペイロードで受信したリクエストを解析する
app.use(express.static(path.join(__dirname, 'public'))); // This is a built-in middleware function in Express. It serves static files and is based on serve-static.HTMLファイルや画像などの静的リソースを提供する
app.use(cookieParser());

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

以下のサイトを参考にさせていただきました。

Express - Node.js web application framework

ミドルウェアの使用 | Express 入門 | CODE Q&A 問題解決 [日本語]

逆引きメモ:expressの使い方 - Qiita

一通り復習が終わったらVue+Nodeで何かwebアプリを作りはじめたいと思います。
ここまで読んでいただいてありがとうございました!!

Vueで書籍管理画面っぽいものを作ってみた

Vueの学習のために書籍管理画面っぽいものを作ってみました。
書籍リストを読む処理にaxiosを使いましたがcodepen上はaxiosを使用しないソースになっています。
axiosを使用したソースはページ下部に載せておきます。

ちなみに書籍リストは以下のユニークなidを持った配列にしました。

[
    { "id":1,  "title": "book01", "price": 100 },
    { "id":2,  "title": "book02", "price": 200 },
    { "id":3,  "title": "book03", "price": 300 },
    { "id":4,  "title": "book04", "price": 400 },
    { "id":5,  "title": "book05", "price": 500 },
    { "id":6,  "title": "book06", "price": 600 },
    { "id":7,  "title": "book07", "price": 700 },
    { "id":8,  "title": "book08", "price": 800 },
    { "id":9,  "title": "book09", "price": 900 },
    { "id":10, "title": "book10", "price": 1000 }
]


仕様

  • 価格絞り込み
  • 数量絞り込み
  • 昇順/降順並び替え
  • 書籍登録
  • 書籍更新
  • 書籍削除
  • 書籍リストのリロード(リロード中にNow Loading表示)
  • 500円以下は10%OFF付ける

価格絞り込み・数量絞り込み

Vueコンストラクタ関数を使ってVueインスタンスを作成します。
Vueインスタンスにはいったんデフォルトで以下のオプションオブジェクトを渡します。

const app = new Vue({
    el: '#app',
    data: {
        budget: 2000,
        amount: 20,
        books: []
    },
)

画面上で入力した値をデータにバインドするためにv-modelディレクティブを使います。

// HTML
<input v-model.number="budget"><span>円以下</span>
<input v-model.number="amount">冊以内に絞り込む


価格絞り込みについて

computedを使います。

  1. filterメソッド(非破壊的)を使って予算以下の書籍リストを構築してreturnする
  2. returnされたリストを表示させる
// computed
matched: funciton() {
    return this.books.filter(books => {
        book.price <= this.budget
    }, this)
}
// HTML
<li v-for="book in matched" :key="book.id">

これで価格の絞り込みが完了です。


数量絞り込みについて

computedを使います。

this.matchからreturnされる配列のうち数量分をsliceする

// computed
amounted: function() {
    return this.matched.slice(0, this.amount)
}
// HTML
<li v-for="book in amounted" :key="book.id">

これで価格の絞り込みからの数量絞り込みが完了です。


昇順/降順並び替えについて

昇順/降順ボタンを押すたびに順序を切り替えるように実装します。

  1. データにflg用のプロパティを持たせてflgの状態で昇順/降順を切り替える
  2. Lodashを使って並び変える
  3. returnされる配列を表示させる
const app = new Vue({
    el: '#app',
    data: {
        budget: 2000,
        amount: 20,
        books: [],
        sort: false // add
    },
)
// computed
sorted: function() {
    return _.orderBy( this.amounted, 'price', this.sort ? 'desc' : 'asc')
}
// HTML
<button @click="sort=!sort">
    <span v-if="sort">降順</span>
    <span v-else>昇順</span>
</button>

これで価格絞り込みと数量絞り込みをした書籍リストを昇順/降順で並び替えることができます。


書籍登録について

  1. 書籍にはそれぞれユニークなidを振っているので書籍リストのlengthに応じてidの値を自動で割り振れるようにlengthを取得するメソッドを用意する
  2. 追加する書籍のプロパティを入力する(idは1で用意したメソッドを使う)
  3. pushメソッドで書籍リストに「入力したプロパティを持つ新しい要素」を追加する
const app = new Vue({
    el: "#app",
    data: {
        budget: 2000,
        amount: 20,
        books: [],
        sort: false,
        titleR: null, // add
        priceR: null  // add
    }
// computed
max: function() {
    return this.books.reduce((a, b)=> {
        return a > b.id ? a : b.id
    }, 0)
}
// methods
regist: function() {
    this.books.push({
        id: this.max + 1,
        title: this.titleR,
        price: this.priceR
    })
    this.titleR = null;
    this.priceR= null;
}
// HTML
<span>書籍名:</span><input v-model="titleR">
<span>価格:</span><input v-model.number="priceR">
<button @click="regist">書籍登録</button>

書籍更新について

  1. 更新したい書籍idと更新後のタイトル、価格を入力する
  2. 更新したい書籍idと一致する要素の配列インデックスを取得する
  3. その配列インデックスのプロパティを$setを使って更新する
const app = new Vue({
    el: "#app",
    data: {
        budget: 2000,
        amount: 20,
        books: [],
        sort: false,
        titleR: null,
        titleU: null, // add
        priceR: null,
        priceU: null, // add
        id: null,     // add
    }
// methods
update: function() {
    if(this.id != "") {
        this.books.forEach(el=> {
            if(el.id == this.id) {
                let target = this.books.indexOf(el);
                this.$set(this.books, target, {
                    id: this.id,
                    title: this.titleU,
                    price: this.priceU
                })
            }
        }, this)
    };
    this.id = null;
    this.titleU = null;
    this.priceU= null;
}
// HTML
<span>書籍No.</span><input v-model.number="id">
<span>書籍名:</span><input v-model="titleU"><br class="spi">
<span>値段:</span><input v-model.number="priceU">円
<button @click="update">書籍更新</button>

書籍削除について

  1. 削除ボタンを押す
  2. 削除ボタンが押された要素の配列インデックスを取得する
  3. spliceでその配列インデックスの要素を取り除く
// methods
delate: function(el) {
    let target = this.books.indexOf(el);
    this.books.splice(target, 1);
}
// HTML
<button @click="delate(book)">削除する</button>

書籍リストのリロード(リロード中にNow Loading表示)について

※ここでは書籍リストを直書きしていますが、axiosを使ってjsonから読ませる方法も次のセクションに記載します。

  1. Vueのウォッチャを使って書籍リストの高さを監視する(データにheightを持たせてデフォルトの高さを0にしておく)
  2. 書籍リストがない場合はNow Loadingの表示を出す
  3. 一秒後に書籍リストをDOMにマウントする
  4. $nextTickを使って書籍リストがDOMにマウントされたら書籍リストの高さを取得する
  5. 取得した高さをstyle属性にバインドする
  6. リロードボタン押下時にいったん書籍リストを空にして2~5の処理を繰り返す
const app = new Vue({
    el: "#app",
    data: {
        budget: 2000,
        amount: 20,
        books: [],
        sort: false,
        titleR: null,
        titleU: null,
        priceR: null,
        priceU: null,
        id: null,
        height: 0, // add
    }
// watch
books: function() {
    console.log(this.$refs.booksCont.getBoundingClientRect().height)
    this.$nextTick(()=> {
        console.log(this.$refs.booksCont.getBoundingClientRect().height)
        this.height = this.$refs.booksCont.getBoundingClientRect().height
    })
}
// methods
loadBooks: function() {
    this.books = []; // リロードボタン押下時のための処理
    setTimeout(()=> {
        this.books = [
            { "id":1,  "title": "book01", "price": 100 },
            { "id":2,  "title": "book02", "price": 200 },
            { "id":3,  "title": "book03", "price": 300 },
            { "id":4,  "title": "book04", "price": 400 },
            { "id":5,  "title": "book05", "price": 500 },
            { "id":6,  "title": "book06", "price": 600 },
            { "id":7,  "title": "book07", "price": 700 },
            { "id":8,  "title": "book08", "price": 800 },
            { "id":9,  "title": "book09", "price": 900 },
            { "id":10, "title": "book10", "price": 1000 }
        ]
    }, 1000)
}
// HTML

// Now Loading箇所
<div class="p-top__loading" v-if="!books.length">
    Now Loading...
</div>

// 高さをbindする箇所
<div :style="{height: height + 'px'}">
    <ul v-cloak ref="booksCont">

// リロードボタン箇所
<button @click="loadBooks" :disabled="!books.length">
    <span>リロード</span>
</button>

書籍リストをaxiosでjsonから読む方法

書籍リストをaxiosでjsonから取得する方法を以下に記載します。
やっていることはjsonから書籍リストを読み込んで1秒後にDOMにマウントする処理をcreatedで実行しています。

// JSON
[
    { "id":1,  "title": "book01", "price": 100 },
    { "id":2,  "title": "book02", "price": 200 },
    { "id":3,  "title": "book03", "price": 300 },
    { "id":4,  "title": "book04", "price": 400 },
    { "id":5,  "title": "book05", "price": 500 },
    { "id":6,  "title": "book06", "price": 600 },
    { "id":7,  "title": "book07", "price": 700 },
    { "id":8,  "title": "book08", "price": 800 },
    { "id":9,  "title": "book09", "price": 900 },
    { "id":10, "title": "book10", "price": 1000 }
]
// methods
loadBooks: function() {
    this.books = []; // リロードボタン押下時のための処理
    axios.get("./json/books.json").then(responce => {
        setTimeout(()=> {
            this.books = responce.data
        }, 1000)
    })
    .catch(e => {
        console.log(e)
    })
}
// created
created: function() {
    this.loadBooks();
}

500円以下は10%OFF付ける

// HTML
<span v-if="book.price < 500 ">10%OFF</span>

分かりづらい部分があるかもしれないので後日リライトするかもしれないです。
あと今はPromiseよりAsync/awaitを使うのが一般的かと思いますので、もう少しモダンな書き方ができるかもしれません。

機能を実装するにあたりこちらの本を参考にさせていただきました!!ありがとうございます!!!
基礎から学ぶ Vue.js
https://www.amazon.co.jp/dp/B07D9BYHMZ/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1

ここまで読んでいただいてありがとうございました!!!