JavaScriptの駐車場管理システム

Oct 20 2020

仮説

以下の料金で有料駐車場があります。 $1 for the first hour and $その後の1時間ごとに0.5。駐車場は10台分です。

必須

次の特徴を備えた駐車場の活動管理システムを作成します。

  • 駐車間隔は、最も近い時間に切り上げられます。
  • 新車が駐車場に入ると、駐車システムは車の登録番号を取り込みます。
  • 車が駐車場を離れると、システムは滞在期間に応じて要約を発行します。要約の一部としてどのような情報が不可欠だと思いますか?
  • すべての顧客は、特定の時間に駐車場に駐車されている車のリストを見ることができます。

技術的要件

  • 実装はJavaScriptを使用して行われ、すべてのデータはメモリに保存され、永続的なストレージは必要ありません。
  • クリーンなコードを書く。
  • コンピューターからのデータの入力と読み取りを可能にするシンプルなGUIを作成します。同じページにデータを表示します。

解決

私は美学のためにBootstrap4を使用しました。スクリプト自体はJavaScriptのみを使用します。jQueryは、Bootstrapのコアクト機能のために含まれています。

let cars = [];
const addCarButton = document.querySelector('#carButton');
const minLicenseeLength = 7;
const payPerHour = 0.5;
const payFirstHour = 1;
const totalPlaces = 10;

const formatDate = (date) => {
  var hours = date.getHours();
  var minutes = date.getMinutes();
  var ampm = hours >= 12 ? 'PM' : 'AM';
  hours = hours % 12;
  hours = hours ? hours : 12;
  minutes = minutes < 10 ? '0' + minutes : minutes;
  var strTime = hours + ':' + minutes + ' ' + ampm;
  return date.getDate() + "/" + (date.getMonth() + 1) + "/" + date.getFullYear() + " " + strTime;
}

const secondsToHours = (d) => {
  d = Number(d);
  let h = Math.ceil(d / 3600);
  return h;
}

const renterTable = () => {
  let results = '';
  for (var i = 0; i < cars.length; i++) {
    let licensee = cars[i].licensee;
    let arrival = formatDate(cars[i].arrival);
    let leave = cars[i].leave === '-' ? '-' : formatDate(cars[i].leave);

    results += `<tr>
          <td>${licensee}</td> <td>${arrival}</td>
          <td>${leave}</td> <td>${showStatus(cars[i])}</td>
          <td class="text-right">${makeBill(cars[i])}</td> <td class="text-right"> <button data-row="${i}" onclick="showSummary(event)" data-toggle="modal" data-target="#myModal" class="btn btn-sm btn-success">Summary</button>
                    </td>
        </tr>`;
  }
  document.querySelector("#parking tbody").innerHTML = results;
}

const showStatus = (car) => {
  return car.isParked ? "Parked" : "Has left";
}

const changeStatus = (event) => {
  cars[event.target.dataset.row].isParked = false;
}

const setLeaveTime = (event) => {
  cars[event.target.dataset.row].leave = new Date(Date.now());
}

const countAvailablePlaces = (event) => {
  document.querySelector('#placesCount').innerHTML = totalPlaces - cars.length;
}

const setClassForBadge = () => {
  let badgeClassName = cars.length == totalPlaces ? 'badge badge-danger' : 'badge badge-success';
  document.querySelector('#placesCount').setAttribute('class', badgeClassName);
}

const calculateHoursBilled = (car) => {
  let arrivedAt = new Date(car.arrival).getTime();
  let leftAt = new Date(car.leave).getTime();
  return secondsToHours((leftAt - arrivedAt) / 1000); //duration in seconds
}

const makeBill = (car) => {
  let hoursBilled = calculateHoursBilled(car);
  let billValue = car.isParked ? "-" : "$" + (payFirstHour + (hoursBilled - 1) * payPerHour); return billValue; } const printSummary = (event) => { let car = cars[event.target.dataset.row]; let sumarryTable = `<table class="table table-bordered m-0"> <tr> <td class="font-weight-bold">Registration number</td> <td>${car.licensee}</td>
        </tr>
        <tr>
          <td class="font-weight-bold">Arrival</td>
          <td>${formatDate(car.arrival)}</td> </tr> <tr> <td class="font-weight-bold">Departure</td> <td>${formatDate(car.leave)}</td>
        </tr>
        <tr>
          <td class="font-weight-bold">Billable hours</td>
          <td>${calculateHoursBilled(car)}</td> </tr> <tr> <td class="font-weight-bold">Bill value</td> <td>${makeBill(car)}</td>
      </tr></table>`;

  document.querySelector('#modalBody').innerHTML = sumarryTable;
}

const showSummary = (event) => {
  changeStatus(event);
  setLeaveTime(event);
  renterTable();
  printSummary(event);

  //Free the parking place, 3 seconds after the summary is released
  setTimeout(function() {
    freeSpot(event);
  }, 3000);
}

const addCar = () => {
  let newLicensee = document.querySelector("#carValue").value;

  let newCar = {
    licensee: newLicensee,
    arrival: new Date(),
    leave: '-',
    isParked: true
  }

  // Add new car to the cars array

  document.querySelector('#message').style.display = 'none';
  if (newLicensee.length >= minLicenseeLength && cars.length < totalPlaces) {
    cars.unshift(newCar);
  } else {
    if (newLicensee.length < minLicenseeLength) {
      document.querySelector('#message').style.display = 'block';
    }
  }

  if (cars.length == totalPlaces) {
    document.querySelector('#carButton').setAttribute('disabled', true);
  }

  setClassForBadge();

  //Update places count
  countAvailablePlaces(event);

  // Empty text box
  document.querySelector("#carValue").value = '';

  // Render the table
  renterTable();
}

const freeSpot = (event) => {
  cars.splice(event.target.dataset.row, 1);
  setClassForBadge();

  if (cars.length == totalPlaces) {
    document.querySelector('#carButton').setAttribute('disabled');
  } else {
    document.querySelector('#carButton').removeAttribute('disabled');
  }

  // Render Table again after delete 
  renterTable();
  //Update places count
  countAvailablePlaces(event);
}

// Add new car to the array
addCarButton.addEventListener('click', addCar);

// Render Table
renterTable();

//Show places count at page load
countAvailablePlaces(event);
#addForm {
  position: relative;
}

#message {
  display: none;
  position: relative;
  font-size: 10px;
  position: absolute;
}

#placesCount {
  font-size: 90%;
}

#parking th,
#parking td {
  white-space: nowrap;
  font-size: 14px;
}

#myModal .font-weight-bold {
  font-weight: 500 !important;
}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

<div class="container">
  <div class="card my-3">
    <div class="card-header px-3 d-flex">
      <h5 class="m-0">Parking management</h5>
      <div class="ml-auto">
        <span id="placesCount" class="badge badge-success"></span> available places
      </div>
    </div>
    <div class="card-body p-0">
      <div class="input-group p-2" id="addForm">
        <input type="text" class="form-control" id="carValue" placeholder="Registration number">
        <div class="input-group-append">
          <button id="carButton" class="btn btn-sm btn-success">Park car</button>
        </div>
        <p id="message" class="text-danger m-0">Registration number invalid</p>
      </div>
      <div class="table-responsive">
        <table id="parking" class="table table-striped m-0">
          <thead>
            <tr>
              <th>Registration no</th>
              <th>Arrival</th>
              <th>Departure</th>
              <th>Status</th>
              <th class="text-right">Bill</th>
              <th class="text-right">Actions</th>
            </tr>
          </thead>
          <tbody></tbody>
        </table>
      </div>
    </div>
  </div>
</div>

<div class="modal" id="myModal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h6 class="modal-title">Sumar Parcare</h6>
        <button type="button" class="close" data-dismiss="modal">&times;</button>
      </div>
      <div id="modalBody" class="modal-body"></div>
    </div>
  </div>
</div>

セキュリティの観点も含め、このアプリケーションに問題はありますか?

ありがとう!

回答

5 CertainPerformance Oct 21 2020 at 00:50

優先constオーバーletとvar。で変数を宣言するletと、コードのすべてのリーダーに、変数がいつでも再割り当てされる可能性があることを示します。コードが機能するために再割り当てが実際に必要でない場合、これは望ましくない認知オーバーヘッドをもたらします-それは心の後ろで常に心配するかもしれません「この変数名はいつでも再割り当てされる可能性があるので、それが今参照しているもの元々割り当てられていたものではない可能性があります。*を参照してください。https://softwareengineering.stackexchange.com/questions/278652/how-much-should-i-be-using-let-vs-const-in-es6

var完全に回避するのが最善です。問題が多すぎて、今日では使用する価値がありません。最も重要なのは、ブロックスコープではなく直感的でない関数スコープがあることです。

文字列のパディングあなたがパッドに文字列の特定の長さ(例えば、ここでは、作るためにいくつかの先行文字と文字列の先頭たい9と10する09と10、それぞれに)、使用する最も適切な方法がありますpadStart

テンプレートリテラルは、複数の変数を文字列に補間する必要がある場合に最適です。多くの場合、のスタイルよりも望ましいです' + someVar + '。

上記の3つのヒントを考慮に入れると、formatDate関数は次のようにリファクタリングできます。

const formatDate = (date) => {
  const hoursMilitary = date.getHours();
  const minutesToDisplay = String(date.getMinutes()).padStart('0', 2);
  const ampm = hoursMilitary >= 12 ? 'PM' : 'AM';
  const hoursToDisplay = (hoursMilitary % 12) || 12;
  const strTime = `${hoursToDisplay}:${minutesToDisplay} ${ampm}`; return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} ${strTime}`;
}

secondsToHoursは、パラメーターを再割り当てすることなく、はるかに簡潔にすることができます。

const secondsToHours = d => Math.ceil(d / 3600);

/非数を数に強制します。あなたが呼び出す必要はありませんNumber上のd最初の。

renterTableここでは多くの改善を行うことができます。まず、関数名は完全に直感的ではありません- renterTable(名詞)と呼ばれるものは何をしますか?HTMLTableElementを含む変数ですか?テーブルを作成して返しますか?いいえ、cars配列内のデータを指定して、DOM内のテーブルを再レンダリングします。代わりに、renderTableまたはrenderRenterTable(動詞)と呼ぶことを検討してください。

ユーザー入力を連結してHTMLを作成するのは安全ではありません。

  results += `<tr>
        <td>${licensee}</td>
        <td>${arrival}</td> <td>${leave}</td>
        <td>${showStatus(cars[i])}</td> <td class="text-right">${makeBill(cars[i])}</td>
        <td class="text-right">
          <button data-row="${i}" onclick="showSummary(event)" data-toggle="modal" data-target="#myModal" class="btn btn-sm btn-success">Summary</button>
                  </td>
      </tr>`;
}
document.querySelector("#parking tbody").innerHTML = results;

これにより、任意のコードが実行される可能性があります。誰かが言ったと想像してみてください:

車を登録するときに次のナンバープレートを差し込んでみてください。次に何が起こるか信じられません!

<img src onerror='alert("evil")'>

ここで、alert("evil")フィッシング詐欺師が望むものに置き換えることができます。そうすると、あまり知性のないユーザーがそれに夢中になり、ログイン情報やアカウントの資金などが危険にさらされる可能性があります。

これはlicensee、最初に有効な文字のみが含まれていることを確認することで修正できます(例:turn :)。

let newLicensee = document.querySelector("#carValue").value;
if (!/^[a-z\d]+$/.test(newLicensee)) {
  // display error message to user: license invalid
  return;
}

しかし、それはまだパッチジョブにすぎません。将来スクリプトが開発された場合、DOMに挿入する前に誤って入力をサニタイズしないのではないかと心配しています。ユーザー入力が含まれる場合、HTMLによる文字列補間を完全に回避し、代わりにtextContent要素がドキュメントに配置された後に要素にを挿入することをお勧めします。例:

const tr = document.querySelector("#parking tbody").appendChild(document.createElement('tr'));
tr.innerHTML = `
  <td></td>
  <td></td>
  <td></td>
  ...
`;
tr.children[0].textContent = licensee;
tr.children[1].textContent = arrival;
tr.children[2].textContent = leave;
// ...

または、さらに良いことに、大規模なプロジェクトの場合は、ReactのようにDOMへの入力の簡潔な補間を可能にするフレームワークの使用を検討してください。

const CarRow = (carInfo) => (
  <tr>
    <td>{carInfo.licensee}</td>
    <td>{carInfo.arrival}</td>
    <td>{carInfo.leave}</td>
    ...

上記のすべてsumarryTableは、同じ種類の連結されたHTMLの脆弱性を持つに適用されます。

スペルスペルはプログラミングで重要です-適切にスペルされた変数名はバグを防ぐのに役立ちます。summaryTable代わりにそれを呼び出します。また、「ライセンシー」という言葉はライセンスを保持している人を指しますが、変数はナンバープレートの文字列を指します。のlicenseID代わりにそれを呼び出すことを検討してくださいlicensee。混乱が少なくなります。

回避innerHTML上記と同様の理由で、innerHTMLHTMLマークアップを挿入する必要がある場合にのみ使用してください。要素にテキストを挿入するだけの場合は、textContent;を使用します。より安全で高速になり、HTMLで特別な意味を持つ文字に問題が発生することはありません。この:

document.querySelector('#placesCount').innerHTML = totalPlaces - cars.length;

することができます

document.querySelector('#placesCount').textContent = totalPlaces - cars.length;

(#placesCount関数が呼び出されるたびに参照を再選択する代わりに、への参照を保存することもできます)

インラインハンドラーを避ける行マークアップ内には、次のものがあります。

<button data-row="${i}" onclick="showSummary(event)" data-toggle="modal" data-target="#myModal" class="btn btn-sm btn-success">Summary</button>

インラインハンドラーには問題が多すぎて、最新のコードで使用する価値はありません。それらは地球規模の汚染を必要とし、他の問題の中でもとりわけ、クレイジーなスコープチェーンを持っています。代わりにJavaScriptを使用してイベントリスナーを適切にアタッチします。ボタンがDOMに挿入されたときにリスナーを各ボタンにアタッチするか、場合によってはイベント委任を使用することをお勧めします。以下のコメントも参照してください。

table.addEventListener('click', (event) => {
  if (!event.target.matches('button')) {
    return;
  }
  // I renamed this from "row" to "rowIndex" to make the variable name more precise;
  // it's not an actual row, it's only an index
  const { rowIndex } = event.target.dataset;
  // Now that the rowIndex has been extracted, grab the car,
  // then pass the car along instead of extracting it inside each function:
  const car = cars[rowIndex];
  changeStatus(car);
  setLeaveTime(car);
  
  renderRenterTable();
  printSummary(car);

  //Free the parking place, 3 seconds after the summary is released
  setTimeout(function() {
    freeSpot(rowIndex);
  }, 3000);
});

可能な場合はドット表記を優先します。より簡潔で読みやすくなります。

document.querySelector('#placesCount').setAttribute('class', badgeClassName);
// ...
if (cars.length == totalPlaces) {
  document.querySelector('#carButton').setAttribute('disabled');
} else {
  document.querySelector('#carButton').removeAttribute('disabled');
}

これらは次のように変更できます。

document.querySelector('#placesCount').className = badgeClassName;
// ...
document.querySelector('#carButton').disabled = cars.length === totalPlaces;

(=== 代わり に常に使用することを忘れないでください==;==実装されているロジックを把握するためにスクリプトの読者が理解する必要のない奇妙なルールがあります)

最新の構文最新の構文と最新のメソッドがソースコードで使用されています。これは優れた機能であり、クリーンで簡潔で読みやすいコードになります。本番コードの場合は、Babelとpolyfillsを使用して、ソースをES5に自動的にトランスパイルし、IEなどの廃止されたブラウザーでもコードを実行できるようにします。