rubytomato's “Getting Started”

Webアプリケーション開発の入門的な記事を投稿していきます。

『入門向け』VSCodeでJavaScriptの学習環境を構築する

はじめに

この記事は、JavaScriptの基礎部分は既に入門書籍や他の入門サイトで学習中で、これから手を動かしてコーディングしながら知識を深めたいという人向けに作成しました。 コードエディターにはVSCodeを利用しますので、VSCodeの使い方を知りたいという人にもおすすめです。 なお、スクリーンショットを多用していますが画面やメニューの内容は2020年5月時点のバージョンのものです。今後バージョンアップによって変わることがありますので予めご了承ください。

環境

この記事の内容はWindows 10で作成、動作確認しています。MacOSユーザーの方は適宜内容を読み替えてください(特にショートカットキーやフォルダーパス)。

VSCode (Visual Studio Code)のインストール

JavaScriptの学習環境にVSCodeを利用するため、まだインストールされていない方は下記の内容を参考にインストールしてください。すでにインストール済みの方は”拡張機能のインストール”まで読み飛ばしてください。

VSCodeとは

VSCode (Visual Studio Code)とは、マイクロソフトが開発している無料で使える軽量コードエディターです。動作が軽いうえに高機能で細かいカスタマイズが可能な点などが評価されて多くの利用者がいます。 JavaScriptのみならずhtml/cssのコーディングに最適なコードエディターなので、本記事でもこれを使って説明します。

ダウンロード

マイクロソフトダウンロードサイトにアクセスしてインストーラーをダウンロードします。

f:id:rubytomato:20200503230215p:plain
Fig.1

Windows版のインストーラーはUser InstallerSystem Installerzipの3種類あります。

  • User Installerはインストールに管理者権限が不要
  • System Installerはインストールに管理者権限が必要
  • zipはZipアーカイブを自身で展開して手動インストールするタイプ

この記事ではUser Installerの64bit版を選択しました。

f:id:rubytomato:20200503230302p:plain
Fig.2

インストール

2020年5月時点のWindows版のダウンロードファイル名はVSCodeUserSetup-x64-1.44.2.exeです。ダウンロードしたインストーラーを実行してインストールします。 基本的にはインストーラーのデフォルト設定のままインストールすれば問題ありません。追加タスクの選択画面(Fig.6)でいくつか設定ができますが、ここはお好みで設定してください。

f:id:rubytomato:20200503230413p:plain
Fig.3

f:id:rubytomato:20200503230428p:plain
Fig.4

f:id:rubytomato:20200503230444p:plain
Fig.5

f:id:rubytomato:20200503230459p:plain
Fig.6

f:id:rubytomato:20200503230514p:plain
Fig.7

f:id:rubytomato:20200503230531p:plain
Fig.8

インストールが完了するとWelcome画面(Fig.9)が立ち上がります。

f:id:rubytomato:20200503230614p:plain
Fig.9

バージョンの確認

メニューバーのHelpAbout でバージョンを確認できます。バージョンを確認するとFig.10の通りVersion 1.44.2 (user setup)がインストールされたことがわかります。

f:id:rubytomato:20200503230631p:plain
Fig.10

拡張機能のインストール

JavaScriptの学習に入る前にいくつか定番の拡張機能をインストールしておきます。 左側のサイドメニューの一番下のアイコンをクリックします。Fig11は拡張機能を管理するメニューで、マーケットプレイスから拡張機能を検索したりインストールすることや、インストールされている拡張機能やアンインストールすることができます。

f:id:rubytomato:20200503230735p:plain
Fig.11

拡張機能マーケットプレイスとは

拡張機能VSCodeの大きな特徴の1つです。拡張機能をインストールすることでVSCodeに新しい機能を追加することができます。どのような拡張機能があるかはマーケットプレイスというサービスで確認できます。

マーケットプレイスにアクセスすると、定番のものや最近人気が出たもの、新しく追加されたものなどを探せます。

Japanese Language Pack for Visual Studio Code

1つ目はメニューやメッセージを日本語化する拡張機能です。

検索フィールドに"japanese"と入力してEnterを押すと、検索結果が表示されます。この中から"Japanese Language Pack for Visual Studio Code"という拡張機能をインストールします。 インストールはFig.12の"Install"という緑色のラベルをクリックするだけです。

f:id:rubytomato:20200503230756p:plain
Fig.12

インストールすると再起動を促されるので再起動します。Fig.13が再起動後のWelcome画面でメニューが日本語化されています。(この画面下の"起動時にウェルカムページを表示"のチェックを外してください)

f:id:rubytomato:20200503230815p:plain
Fig.13

Visual Studio IntelliCode

次にインストールするのはAIを活用したIntelliSenseの機能が利用できる拡張機能です。コード補完で表示される候補がAIによって最適化されます。

f:id:rubytomato:20200503230836p:plain
Fig.14

Live Server

インストールするとローカルPCで開発用途の簡易HTTPサーバーを起動することができ、コーディング中のhtmlやJavaScriptなどの動作確認が簡単にできるようになります。 またホットリロードというファイルの変更を検知してブラウザ上のページを自動的にリロードする機能もあります。

f:id:rubytomato:20200503230946p:plain
Fig.15

とりあえず現時点ではこの3つだけインストールします。 これでVSCode拡張機能のインストールは完了です。

JavaScript学習用のプロジェクトを作成

学習環境が整ったので、以降はJavaScriptの学習に関する内容になります。 まずはVSCodeソースコードを管理できるようにプロジェクトを作成します。といってもソースコードを保存するフォルダーを作成して、そこにhtmlファイルとjsファイルを作成するだけです。

プロジェクトフォルダーの作成

この記事ではexercise-jsというフォルダーを作成しました。(作成場所やフォルダー名は任意です) 以後はこの場所にプロジェクトフォルダーがあるという前提で説明を行いますので、任意の場所にフォルダーを作成したい場合は適宜読み替えてください。

Fig.16はVSCodeexercise-jsを開いた直後の画面です。

f:id:rubytomato:20200503231006p:plain
Fig.16

htmlファイルの作成

JavaScriptの動作確認はブラウザ(この記事ではChromeを使用します)で行いますので、そのためのhtmlファイルを作成します。 Fig.17の"新しいファイル"を作成するアイコンをクリックし、ファイル名にindex.htmlと入力してhtmlファイルを作成します。

f:id:rubytomato:20200503231836p:plain
Fig.17

空のhtmlファイルがエディタ画面に表示されるのでFig.18のようにhtmlと入力し、候補からhtml:5を選択してEnterを押すとテンプレートのコードが展開されます。

f:id:rubytomato:20200503231905g:plain
Fig.18

titleタグを下記のように修正します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>exercise-js</title>
</head>
<body>

</body>
</html>

次にFig.19のようにbodyタグの中でdiv#appと入力してEnterを押すとコードが展開されます。さらにdivタグの中でh1と入力してEnterを押すとh1タグが展開されます。 このhtmlタグの展開はemmetという機能です(emmetについては後述します)。

f:id:rubytomato:20200503232011g:plain
Fig.19

この状態でLive Serverを立ち上げてブラウザでindex.htmlを表示してみます。 エディタ画面上で右クリックしメニューからOpen with Live Serverをクリックします。

f:id:rubytomato:20200503232034p:plain
Fig.20

ブラウザが起動し、Fig.21の画面が表示されたと思います。

f:id:rubytomato:20200503232049p:plain
Fig.21

続いて下記のようにh1タグの下にdivタグを追加してファイルを保存すると、自動的にページがリロードされて表示が変わります。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>exercise-js</title>
</head>
<body>
    <div id="app">
        <h1>exercise-js</h1>
        <div>hello javascript</div>       <!-- 追加する -->
    </div>
</body>
</html>

f:id:rubytomato:20200503232330p:plain
Fig.22

emmetについて

上記でも触れたemmetですが、これはVSCodeにデフォルトで組み込まれているhtml/cssを簡単にコーディングできる機能です。

たとえば div#app と入力してEnterを押すと

<div id="app"></div>

と展開されます。入力したdivの部分がhtmlタグを表し、つぎの#の部分がidを意味し、残りのappがid名となります。

同様に div.contents と入力してEnterを押すと

<div class="contents"></div>

と展開されます。.(ピリオド)の部分がclassを意味し、残りのcontentsがclass名となります。

ちなみにclass名は複数付けることができますが、.でclass名を繋げることで複数指定できます。 例えば div.card.infoと入力してEnterを押すと

<div class="card info"></div>

と展開されます。

ネストしたタグを表現する

ul,liのようにネストしたタグもemmetを使って簡単にコーディングできます。 ul>liと入力してEnterを押すと

<ul>
    <li></li>
</ul>

と展開します。

liタグを複数個展開したい場合は個数を指定することもできます。個数は*3のように表記し ul>li*3と入力してEnterを押すと

<ul>
    <li></li>
    <li></li>
    <li></li>
</ul>

と、ulタグの中にliタグが3つ展開されます。

emmetの機能は他にもありますので、"VSCode emmet"等のキーワードで検索して調べてみてください。

jsファイルの作成

*.jsファイルはjsというフォルダーに作成するようにします。 Fig.23の"新しいフォルダー"を作成するアイコンをクリックし、フォルダー名にjsと入力してjsフォルダーを作成します。

f:id:rubytomato:20200503232355p:plain
Fig.23

そのjsフォルダにmain.jsという名前のjsファイルを作成しFig.24のように1行だけコードを記述します。

f:id:rubytomato:20200503232417p:plain
Fig.24

次にindex.htmlを開き、下のgifのようにbodyタグの直前にscriptタグを追加します。

f:id:rubytomato:20200503232438g:plain
Fig.25

ファイルを保存するとページがリロードされます。 JavaScriptconsole.log(...)の出力を確認するには、ブラウザでF12(またはCtrl + Shift + i)を押して開発者ツールを開きconsoleタブを選択します。 consoleタブに"it works!"という文字列が表示されていれば成功です。

f:id:rubytomato:20200503232459p:plain
Fig.26

Live Serverについて

Live Serverを一度起動すると、ステータスバーにLive Serverのステータスが表示されるようになります。 Fig.27はポート5500でLive Serverが起動中であることを示していて、ここをクリックすると待機中になります。

f:id:rubytomato:20200503232515p:plain
Fig.27

待機中はFig.28のような表示になり、以降はここをクリックするだけで起動と待機を切り替えることができます。

f:id:rubytomato:20200503232532p:plain
Fig.28

設定

Live Serverの設定はユーザー設定から変えることができます。ユーザー設定はCtrl + ,を押すと表示されます(Fig.29)。 Live Serverの設定項目を探すには検索フィールドに"live server"と入力して絞り込みを行います。

f:id:rubytomato:20200503232751p:plain
Fig.29

主な設定項目

Custom Browser

Live Serverで使用するブラウザーを指定します。デフォルトはお気に入りのブラウザです。

Specify custom browser settings for Live Server. By Default it will open your default favorite browser.

File

Live Serverで開くファイルを指定します。デフォルトはindex.htmlです。

When set, serve this file (server root relative) for every 404 (usefull for single-page applications)

Full Reload

CSS変更時に完全なページのリロードを行います。デフォルトでは完全な再読み込みを行わずに変更のあるCSSを挿入します。

By Default Live Server inject CSS changes without full reloading of browser. you can change this behviour by making this setting as 'true'

Host

Live Serverが使用するホストを指定します。デフォルトは"127.0.0.1"です。 "localhost"に代えたい場合は"localhost"と指定します。

To swith between localhost or '127.0.0.1' or anything else. Default is '127.0.0.1'

Port

Live Serverが使用するポートを指定します。デフォルトは5500です。 0を指定するとポートはランダムになります。

Set Custom Port Number of Live Server. Set 0 if you want random port.

Root

ルートディレクトリを指定します。デフォルトはワークスペースです。 ワークスペース内のdistをルートに代えたい場合は、"/dist"と指定します。

Set Custom root of Live Server. To change root the server to sub folder of workspace, use '/' and relative path from workspace.

変更した項目

設定を変えると、項目の左側に青い線が表示されます。Fig.30はポートを変更したときの状態です。

f:id:rubytomato:20200503232907p:plain
Fig.30

ファンクションを作成する

index.htmlを開きFig.31のようにdivタグを追加します。

f:id:rubytomato:20200503232924g:plain
Fig.31

次にmain.jsを開き下記のコードを追記して保存します。

function createElement(message = "ワールド") {
    const template = `<p>
      hello ${message}
    </p>`
    return template
}

const message = createElement("world")

const contents = document.getElementsByClassName("contents")[0]
contents.innerHTML = message

Live Serverを起動していれば、ページがリロードされて"hello world"が表示されたと思います。

f:id:rubytomato:20200503233034p:plain
Fig.32

テンプレートリテラル

createElementファンクションでtemplateという名前の変数の定義していますが、この変数の初期化にテンプレートリテラルという構文を使っています。 テンプレートリテラルはECMAScript2015(ES6)で追加された構文で、文字列リテラルにプレースフォルダ(${ })を含めることができ、そこに変数や式を埋め込むことができます。

デフォルト引数

createElementファンクションのmessage引数にmessage = "ワールド"のようにデフォルト値が与えられていますが、これもECMAScript2015(ES6)で追加されたデフォルト引数(またはデフォルトパラメータ)という構文です。 下記のように呼び出し時にパラメータを渡さなかった場合、デフォルト値が利用されます。

const message = createElement()

jsファイルを分離する

main.jsにコーディングしたfunctionを別のファイルに分けてみます。 jsフォルダにsub.jsというファイルを作成し、main.jsから下記のコードを移動させます。

下記がsub.jsのコードです。

function createElement(message = "ワールド") {
    const template = `<p>
      ${message}
    </p>`
    return template
}

下記がmain.jsのコードです。

console.log("it works!")

const message = createElement("world")

const contents = document.getElementsByClassName("contents")[0]
contents.innerHTML = message

次にindex.htmlを開きmain.jsを読み込んでいるscriptタグの下にsub.jsを読み込むscriptタグを追加します。

<script src="./js/main.js"></script>
<script src="./js/sub.js"></script>   <!-- 追加 -->

ファイル保存後にブラウザで動作確認すると"hello world"というメッセージが表示されなくなっていると思います。 consoleタブを見るとFig.33のようなエラーメッセージが表示されていて、このメッセージから"createElement"が定義されていないことが原因だとわかります。

f:id:rubytomato:20200503233102p:plain
Fig.33

これを厳密に言うと"createElement"は定義されますが、下記のようにsub.jsで"createElement"が定義される前に、main.js内で"createElement"ファンクションが呼び出されているため、ということになります。

<script src="./js/main.js"></script>  <!-- createElementファンクションの呼び出し -->
<script src="./js/sub.js"></script>   <!-- createElementファンクションの定義 -->

この問題を解決するにはjsファイルの読み込み順を下記のように入れ替え、先に"createElement"ファンクションの定義を行うようにします。

<script src="./js/sub.js"></script>   <!-- createElementファンクションの定義 -->
<script src="./js/main.js"></script>  <!-- createElementファンクションの呼び出し -->

ちなみに、行の入れ替えはVSCodeのショートカットキーを使うと簡単にできます。 入れ替えたい行にカーソルを置いた状態で、現在の行を上の行と入れ替えるには上矢印キー、もしくは下の行と入れ替えるには下矢印キーを押します。

Fig.34はsub.jsを読み込んでいる行を上の行と入れ替えている例です。

f:id:rubytomato:20200503233122g:plain
Fig.34

行を入れ替えたらファイルを保存し再度ブラウザで動作確認を行います。これでページに"hello world"というメッセージが表示されていると思います。 このようにJavaScriptは上から下へ順番に解釈されていくので定義順が重要です。

別の解決方法 (document.addEventListener)

上記でjsファイルの読み込み順を変えて解決しましたが、別にdocument.addEventListenerを使って解決する方法があります。この方法ではjsファイルの読み込み順を変える(意識する)必要はありません。

<script src="./js/main.js"></script>  <!-- createElementファンクションの呼び出し -->
<script src="./js/sub.js"></script>   <!-- createElementファンクションの定義 -->

main.jsを下記のように書き換え、コード全体をdocument.addEventListener("DOMContentLoaded", function(event){ ... })で囲みます。

document.addEventListener("DOMContentLoaded", function(event) {

    const message = createElement()
    const contents = document.getElementsByClassName("contents")[0]
    contents.innerHTML = message

})

このコードは大別すると3つの要素に分かれています。

document.addEventListener("DOMContentLoaded", function(event) { ... })
^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^^
 |                          |                  |
 |                          |                  +--- (3)
 |                          |
 |                          +---------------------- (2)
 |
 +------------------------------------------------- (1)

1) document.addEventListener

addEventListenerは対象オブジェクトに、何らかのイベントが発生したときに、実行する何らかのファンクションを追加します。 この例での対象オブジェクトはdocumentです。つまりdocumentオブジェクトに(2)任意のイベントが発生した時に、実行する(3)任意のファンクションを追加します。

2) 任意のイベント

イベントの種類を表しています。

このDOMContentLoadedイベントはdocumentオブジェクトのイベントで、HTMLが完全に読み込まれ解釈された時点で発生します。またJavaScriptの読み込みも完了している(例外があります)ので、定義しているファンクションも使えます。ただし、まだスタイルシート、画像、サブフレームの読み込みは終わっていない可能性があります。

3) 任意のファンクション

対象のオブジェクトに指定したイベントが発生したときに実行されるファンクションを指定します。

別の解決方法 (import/export)

さらにもう1つ別の解決方法があります。それはEcmaScript2015(ES6)で追加された、import/export構文を使用する方法です。

sub.jsでexportを使ってファンクションを公開します。

export function createElement(message = "ワールド") {
    const template = `<p>
      hello ${message}
    </p>`
    return template
}

main.jsではimportを使ってsub.jsから公開されているファンクションを読み込みます。 このように前述のdocument.addEventListener("DOMContentLoaded", function(event) { ... })は使わなくても動作しますが、

import { createElement } from "./sub.js"

const message = createElement()
const contents = document.getElementsByClassName("contents")[0]
contents.innerHTML = message

コードの堅牢制(バグの発生のしにくさ)を考えると、イベントを使った方がいいと思いますので下記のようにします。

import { createElement } from "./sub.js"

document.addEventListener("DOMContentLoaded", function(event) {

    const message = createElement()
    const contents = document.getElementsByClassName("contents")[0]
    contents.innerHTML = message

})

最後にhtmlファイルを修正します。htmlファイルで読み込む必要があるのはmain.jsだけなので、sub.jsを読み込むscriptタグは削除します。 なおimport/exportを使う場合はscriptタグにtype=moduleという記述が必要なので追記します。

<script src="./js/main.js" type="module"></script>  <!-- createElementファンクションの呼び出し -->
<!-- <script src="./js/sub.js"></script> -->  <!-- 読み込み不要 -->

classを使う

次にJavaScriptのclassを使ったコードを学習していきます。class構文もECMAScript2015(ES6)で追加された構文です。

jsフォルダーにitem.jsというファイルを作成し、下記のコードを記述します。main.jsで利用するためexportを付けています。

export class Item {
    // コンストラクタ
    constructor(id, name, price) {
        this.id = id
        this.name = name
        this.price = price
    }
    // メソッド
    toString() {
        return `id:${this.id} name:${this.name} price:${this.price}`
    }
}

main.jsを下記のように修正します。先頭の方でitem.jsのItemクラスをインポートします。 Itemクラスのオブジェクトを生成するにはnew Item( ... )としてnew演算子を使用します。

import { createElement, setItemRow } from "./sub.js"
import { Item } from "./item.js"  // ← 追加

document.addEventListener("DOMContentLoaded", function(event) {
    console.log("DOM fully loaded and parsed", event)

    const message = createElement()
    const contents = document.getElementsByClassName("contents")[0]
    contents.innerHTML = message

    // ↓ 追加
    const apple = new Item(1, "apple", 100)
    const orange = new Item(2, "orange", 80)
    const grape = new Item(3, "grape", 120)
    const items = [apple, orange, grape]
    // ↑ 追加

})

itemsという配列に格納したItemクラスのオブジェクトの文字列情報を、ItemクラスのtoStringメソッドで出力してみます。

これまで配列の操作には

forを使う方法

for (let i=0; items.length>i; i++) {
    console.log(items[i].toString())
}

forEachを使う方法

items.forEach(item => {
    console.log(item.toString())
})

がありましたが、ECMAScript2015(ES6)からfor ofという構文も追加されています。

for (const item of items) {
    console.log(item.toString())
}

せっかくなのでItemクラスのオブジェクトをHTMLのtableで表示するように修正を加えてみます。

index.htmlを修正してtableタグを追加します。tbody内にデータを出力するのはJavaScriptで行うので空の状態です。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>exercise-js</title>
</head>
<body>
    <div id="app">
        <h1>exercise-js</h1>
        <div>hello javascript</div>
        <div class="contents"></div>
        <!-- ↓ 追加 -->
        <table class="item">
            <thead>
                <tr>
                    <th>id</th>
                    <th>name</th>
                    <th>price</th>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
        <!-- ↑ 追加 -->
    </div>
    <script src="./js/main.js" type="module"></script>
</body>
</html>

sub.jsを修正してtbody内にtr、tdタグを出力するファンクションを追加します。trタグを出力するcreateItemRecordファンクションはmain.jsで使用するのでexportを付けているのに対して、tdタグを出力するcreateItemDataファンクションはこのsub.js内でしか使用しないためexportを付けません。このようにexportを付けるかどうかは、そのファンクションの使用範囲(公開範囲)で決めます。

export function createItemRecord(item) {
    const id = createItemData(item.id)
    const name = createItemData(item.name)
    const price = createItemData(item.price)

    const tr = document.createElement("tr")
    tr.appendChild(id)
    tr.appendChild(name)
    tr.appendChild(price)
    return tr
}

function createItemData(value) {
    const td = document.createElement("td")
    const nd = document.createTextNode(value)
    td.appendChild(nd)
    return td
}

main.jsの先頭のimport文をこのように修正してcreateItemRecordファンクションをインポートします。

import { createElement, createItemRecord } from "./sub.js"

下記はitems配列のデータを使ってtbodyタグ内にtr>tdタグを追加するコードです。

const apple = new Item(1, "apple", 100)
const orange = new Item(2, "orange", 80)
const grape = new Item(3, "grape", 120)
const items = [apple, orange, grape]

const itemTable = document.querySelector("table.item > tbody")
for (const item of items) {
    const itemRow = createItemRecord(item)    
    itemTable.appendChild(itemRow)
}

上記のコードを実行すると、ブラウザにFig35のようなテーブルが表示されていると思います。

f:id:rubytomato:20200503233929p:plain
Fig.35

CSSでスタイル

プロジェクトの直下にcssというフォルダーを作成し、そこにmain.cssというファイルを作成します。 とりあえず何も書いていないCSSファイルのままで、index.htmlを修正しheadタグ内にlinkタグを追加します。

<link rel="stylesheet" href="./css/main.css">

次にmain.cssファイルにスタイルを記述していきます。まず最初に下記のスタイルを記述してファイルを保存します。

* {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

#app {
    margin: 20px;
}

.contents {
    margin: 20px 0;
    font-size: 20px;
}

Live Serverを動かして動作確認をしていれば、main.cssファイルの保存を行ったタイミングでブラウザ上のページにスタイルが当たったことがわかると思います。 次にtableのスタイルを追加します。

table {
    border-collapse: collapse;
    font-size: 18px;
}

th {
    background-color: black;
    color: white;
}

th, td {
    padding: 0.5em 10px 0.5em;
    border-top: 1px solid #666;
}

tr:last-child td {
  border-bottom: 1px solid #666;  
}

td:last-child {
    text-align: right;
}

ファイルを保存するとページがリロードされてFig.36のようにスタイルが適用されたと思います。

f:id:rubytomato:20200503233951p:plain
Fig.36

さいごに

VSCodeを使ったJavaScriptの学習環境の構築とVSCodeの簡単な使い方の説明は以上になります。 これまでに作成したソースコード全体は下記の通りです。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./css/main.css">
    <title>exercise-js</title>
</head>
<body>
    <div id="app">
        <h1>exercise-js</h1>
        <div>hello javascript</div>
        <div class="contents"></div>
        <table class="item">
            <thead>
                <tr>
                    <th>id</th>
                    <th>name</th>
                    <th>price</th>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
    </div>
    <script src="./js/main.js" type="module"></script>
</body>
</html>

main.css

* {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

#app {
    margin: 20px;
}

.contents {
    margin: 20px 0;
    font-size: 20px;
}

table {
    border-collapse: collapse;
    font-size: 18px;
}

th {
    background-color: black;
    color: white;
}

th, td {
    padding: 0.5em 10px 0.5em;
    border-top: 1px solid #666;
}

tr:last-child td {
  border-bottom: 1px solid #666;  
}

td:last-child {
    text-align: right;
}

main.js

import { createElement, createItemRecord } from "./sub.js"
import { Item } from "./item.js"

console.log("it works!")

document.addEventListener("DOMContentLoaded", function(event) {

    const message = createElement("world")
    const contents = document.getElementsByClassName("contents")[0]
    contents.innerHTML = message

    const apple = new Item(1, "apple", 100)
    const orange = new Item(2, "orange", 80)
    const grape = new Item(3, "grape", 120)
    const items = [apple, orange, grape,]

    const itemTable = document.querySelector("table.item > tbody")
    for (const item of items) {
        const itemRow = createItemRecord(item)
        itemTable.appendChild(itemRow)
    }

})

sub.js

export function createElement(message = "ワールド") {
    const template = `<p>
      hello ${message}
    </p>`
    return template
}

export function createItemRecord(item) {
    const id = createItemData(item.id)
    const name = createItemData(item.name)
    const price = createItemData(item.price)

    const tr = document.createElement("tr")
    tr.appendChild(id)
    tr.appendChild(name)
    tr.appendChild(price)
    return tr
}

function createItemData(value) {
    const td = document.createElement("td")
    const nd = document.createTextNode(value)
    td.appendChild(nd)
    return td
}

item.js

export class Item {
    // コンストラクタ
    constructor(id, name, price) {
        this.id = id
        this.name = name
        this.price = price
    }
    // メソッド
    toString() {
        return `id:${this.id} name:${this.name} price:${this.price}`
    }
}