JavaScript
Node.js
es6
webpack
babel

新人にドヤ顔で説明できるか、今風フロントエンド開発ハンズオン(Git/Node.js/ES6/webpack4/Babel7)

概要

  • 今風の手法でJavaScriptアプリを作ろうとすると色々ツールがあって便利な反面、複雑でわからないことがたくさんあります。
  • わからないことがあったら、それを放置せず、しっかり理解して大いに寄り道しつつブラウザで動作するJavaScriptアプリをゼロから作っていきます
  • ブラウザ上で動作するフロントエンドアプリを作ったら、ライブラリ化してnpmモジュールとして公開します

対象読者=今風のJavaScript開発の入門者、初心者

  • 10年前からタイムトラベルしてきたひと
  • ブラウザ用アプリを作りたいが今風の手法の初心者(jQueryだけでなんとか生きてきた人とか)
  • Node.jsの環境をつかってフロンドエンドアプリかいているけど、「何となく」理解している人
  • 来年の新人教育係

キーワード

本投稿では、以下のようなキーワードが出てきます。

Node.js、npm、ES6(ECMAScript6)、webpack4、Babel v7、ソースマップ、クラス、JSでオブジェクト指向

ソースコード

この記事のソースコード(最終版)は https://github.com/riversun/es6hello.git にあります。

クローンしてサンプルを実行する場合は、以下のようにします。

git clone https://github.com/riversun/es6hello.git
npm install
npm start

準備

node/npmのインストール

node/npmをそれぞれの環境にあわせてインストールしておく

私のバージョンは以下のとおり。 -vコマンドでバージョンを表示できる

コマンドラインで以下を入力
node -v
v8.11.4

npm -v
5.6.0

npmプロジェクトの作成

プロジェクトの作業ディレクトリを作成

まずプロジェクトの作業ディレクトリを作る
任意の名前をつける。ここでは es6helloとする

コマンドラインで以下を入力
mkdir es6hello
cd es6hello

npm init でnpmプロジェクトを初期化

npm initをして、このディレクトリをnpmプロジェクトにする

コマンドラインで以下を入力
npm init

npm initと入力すると以下のようになる

This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (es6hello)

いろいろきかれるが、後から変更できるのでここではとりあえずすべてエンターでOK。
(エンターを9回程度押す)

package name: (es6hello)?
version: (1.0.0)?
description:?
entry point: (index.js)?
test command:?
git repository:?
keywords:?
license: (ISC)?
About to write to /dev/es6hello/package.json:

{
  "name": "es6hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Tom Misawa <riversun.org@gmail.com> (https://github.com/riversun)",
  "license": "ISC"
}

Is this ok? (yes)?

これでディレクトリに package.json が生成される

package.json
{
  "name": "es6hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Tom Misawa <riversun.org@gmail.com> (https://github.com/riversun)",
  "license": "ISC"
}

これでnpmプロジェクト完成!

現在のディレクトリ構成

es6hello
├── package.json

最後にnpmモジュールとして公開したいから、npmプロジェクトをGit管理下におき、GitHubにpushする

なぜGitの部分まで説明しているかというと、最終的に作ったjsを、外部から使えるnpmモジュールとして公開することをもくろんでいるのでGit管理の説明をする。
npmモジュールにすることに全く興味がなければ、Gitの部分はすべて読み飛ばしてOK。

ソースコードのバージョン管理をするためにディレクトリをGit管理下に置く。
リモート側はGitHubを使う

ディレクトリをGit管理下に置く

このディレクトリをGit管理下におく

git init

Initialized empty Git repository in /dev/es6hello/.git/


このディレクトリ以下のすべてのファイルをGit管理下におく
(といっても、まだpackage.jsonしかない)

git add -A

ちなみに、git add -Aは以下をステージ領域に追加という意味

  • 新規追加されたファイルやフォルダ
  • 変更があったファイルやフォルダ
  • 削除されたファイルやフォルダ


package.jsonをコミットする

git commit -m "first commit"

[master (root-commit) 5cee35d] first commit
 1 file changed, 11 insertions(+)
 create mode 100644 package.json

GitHubに初回のpushをする

ついでにGitのリモートリポジトリにpushするところまでやっておく

GitHubにリポジトリを作る

image.png

GitHubにリポジトリができたらURLをコピーする

image.png

ローカルGitのリモート側をGitHubに設定する

リモート名:originにGitHubのリポジトリをひもづける

git remote add origin https://github.com/riversun/es6hello.git

これでリモート originにgithubにつくったリポジトリがひもづいた。

現在のブランチをGitHubにpushする

現在のブランチはデフォルト設定なので masterブランチとなっている

git branch
* master

現在のブランチをリモート側(origin)のmasterブランチにpushする
(pushするときにupstreamブランチを指定する)

git push --set-upstream origin master

Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 411 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote:
remote: Create a pull request for 'master' on GitHub by visiting:
remote:      https://github.com/riversun/es6hello/pull/new/master
remote:
To https://github.com/riversun/es6hello.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.

以下で、現在のブランチがどのupstreamブランチを追跡するか確認する

git branch -vv
* master 5cee35d [origin/master] first commit

upstreamブランチが設定できていることが確認できた。

また、GitHub側にもちゃんとpushできていることを確認。

image.png

以降は git push でpush可能。

これでひとまずローカル側とリモート側にGitの環境が整った。

webpackを使えるようにする

早くJavaScriptでコードを書きたいところだが、まずは最低限の環境をつくるところからはじめる。

まずはwebpack導入から。

webpackとは何か

webpackとはモジュールバンドラーと呼ばれるモノで、要は複数のjsファイルを良い感じに処理して1つにまとめてくれるツールのこと。

なぜwebpackが必要か

大規模なソフトを開発する場合には、ソースコードを複数のファイルに分割して開発する。
分割することをモジュール化といったりする、また分割された1つ1つのファイルをモジュールと呼んだりする。

以前は、下のようにブラウザからjsファイルを読み込むときに複数のjsファイルをscriptタグで列挙して読み込むということが割と一般的だった。

scriptタグで各種モジュールを読み込んでいるhtml
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Hello world!</h1>

<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>

</body>
</html>

読み込むモジュールが3つくらいなら、これも特に問題はないが、これが増えてくるとどのモジュールとどのモジュールの依存関係があるか、どのモジュールを先に読み込むか、など問題がでてくる。
またnpmで外部公開されているパッケージを取り込むときにもかなり面倒になってくる。

Node.jsのサーバーサイド向け仕様では、こうした問題に対応するためにrequireで依存モジュールを解決したり、npmで外部パッケージを取り込んで利用したりが簡単にできるようになった。
JavaScriptも他の言語プラットフォーム(JavaやRubyなど)同様の開発エコシステムが急速に(※)整備された。
(※私がjsに初めて触れたのは1990年代後半なので、急速に整備されたように感じるw)

そうした便利な機能をフロントエンドつまりブラウザ用のJavaScript開発でも使えるようにしてくれるのがwebpackとなる。(もちろんnpm等も仕組みもあわせてつかう)

いちばんシンプルにwebpackをとらえるなら以下のように複数のjsを1つにまとめてくれる、機能となる。

image.png

webpackと同じようなことをしてくれるツールはいくつかある(以前流行っていたbrowserifyや最近でてきたparcelなど)が現時点でいちばんメジャーなのでこちらを使う。最新のバージョンは webpack4系となる。

webpackの導入準備

早速webpackをインストールしていく。

npm installの使い方

コマンドラインにnpm install --save-dev webpack webpack-cli webpack-dev-serverと入力する。

npm install --save-dev webpack webpack-cli webpack-dev-server

npm WARN es6hello@1.0.0 No description
npm WARN es6hello@1.0.0 No repository field.

+ webpack-cli@3.1.0
+ webpack-dev-server@3.1.8
+ webpack@4.19.1

updated 3 packages in 19.837s

するとインターネットから3つのパッケージ(webpack webpack-cli webpack-dev-server)が自動的にダウンロードされる。
どれもwebpack関連のパッケージとなる。

しばらく待つと、webpackのインストールが終了する。

現在のディレクトリ構成

es6hello
├── node_modules
├── package.json
└── package-lock.json

node_modulesというディレクトリとpackage-lock.jsonというファイルが生成された模様。
これは後で説明する。

webpackのインストールが終わると、package.jsonには以下が自動的に追加される。

package.json追加分
 "devDependencies": {
    "webpack": "^4.19.0",
    "webpack-cli": "^3.1.0",
    "webpack-dev-server": "^3.1.7"
  }

npm install --save-dev webpack webpack-cli webpack-dev-serverの意味だが、
npm installコマンドはnpmのパッケージをインストールしてね、という意味、
そのパッケージは、--save-devは開発時のみに使うという意味となる。

npm install --save-dev パッケージ名 パッケージ名 パッケージ名のように複数のパッケージを同時に指定することができる。

package.jsonのdevDependencies意味とは?

上のpackage.jsonの追加分にあったdevDependencies以下には、このプロジェクトで利用するパッケージが記述される。プロジェクトがこのパッケージに依存しているので依存パッケージとも呼ぶ。

さきほどのパッケージインストール時には --save-devを指定して開発時のみにつかうことを指定したが、それがpackage.jsonではdevDependenciesとなる。

 "devDependencies": {
    "webpack": "^4.19.0",
    "webpack-cli": "^3.1.0",
    "webpack-dev-server": "^3.1.7"
  }

devDependencies以下にある "webpack": "^4.19.0""[パッケージ名]":"^[バージョン]" の意味となる。

バージョンのところにある「^」(キャレット)に注目。「おいおい、^は何だよ」っていう話だが、これはキャレット表記と呼びバージョン指定をするときに特別な意味をもっている。

さて、このキャレット表記の意味だが、^4.19.0は、パッケージのバージョン4.19.0≦ X<5.0.0の範囲ならOKという意味になる。(こちら https://docs.npmjs.com/misc/semver に詳しい説明がある)

それだけでは意味がよくわからないので、まずこのバージョン表記の意味からみていく。

npmパッケージのバージョン、たとえば4.19.0というバージョン名の付け方はセマンティックバージョニングというルールに従っている。

↓のように、それぞれの数字の意味が メジャーバージョン.マイナーバージョン.パッチバージョンの意味をもつ。

image.png

ちなみに、後方互換とはAPIのバージョンが上がっても、旧APIと互換性が保たれるという意味。

さきほどにもどると、キャレット表記つまり^4.19.04.19.0≦ X<5.0.0の範囲ということで「メジャーバージョンは固定するよ、ただマイナーバージョンやパッチバージョンは変化するよ(上がる)」という意味になる。

メジャーバージョンが固定されるので、パッケージを作った人がセマンティックバージョニングの精神にきちんと従っていれば理屈上は後方互換が担保される。

ただし、これは、あくまでも理屈。
人間がやることなのでマイナーバージョンやパッチバージョンを上げるような更新の場合でもうっかり忘れで後方互換が破綻する場合もある。

そのあたりの課題を解決するために考えられたのが次にでてくる package-lock.jsonの話につながる。

package-lock.jsonとは何か

現在は↓のようなディレクトリ構成になっており、package-lock.jsonが自動的に追加されている。

現在のディレクトリ構成

es6hello
├── node_modules
├── package.json
└── package-lock.json

さきほどキャレット表記でメジャーバージョンを固定することで後方互換が理屈上は担保されていたが、実際には、パッチバージョンを変えたら動かなくなってしまった、みたいなことが起こっている。

また、ある依存パッケージが別の依存パッケージに依存しているいわゆるツリー状(入れ子状)の依存関係になっていることが良くあるため、ツリーのどこかにある依存パッケージのマイナーバージョンやパッチバージョンがあがったりすると、イッキに動かなくなってしまうことが起こりえる。

なので、やっぱり依存パッケージのバージョン番号は メジャー・マイナー・パッチ すべて固定(ロック)しようよ、というのがpackage-lock.jsonのお役目となる。

最上位のパッケージだけ固定しても仕方がないので、依存パッケージの依存パッケージといったツリー上の依存関係にある全パッケージのバージョン情報がpackage-lock.jsonに書き出される。

例えば、webpack 4.19.1 の依存パッケージのツリーは以下のようになる。ツリーをたどっていくとなんと1000個以上の依存パッケージがあり、深くネストしている。

ここでは参考までにnpm-remote-lsというツールをつかって、依存パッケージのツリーを表示してみた。
(依存関係をグラフィカルに表示してくれるサイトもある。)

npm install -g npm-remote-ls
npm-remote-ls webpack

 webpack@4.19.1
   ├─ acorn-dynamic-import@3.0.0
   │  └─ acorn@5.7.3
   ├─ ajv-keywords@3.2.0
   ├─ @webassemblyjs/helper-module-context@1.7.6
   │  └─ mamacro@0.0.3
   ├─ @webassemblyjs/ast@1.7.6
   │  ├─ @webassemblyjs/helper-module-context@1.7.6
   │  ├─ mamacro@0.0.3
   │  ├─ @webassemblyjs/helper-wasm-bytecode@1.7.6
   │  └─ @webassemblyjs/wast-parser@1.7.6
   │     ├─ @webassemblyjs/helper-api-error@1.7.6
   │     ├─ mamacro@0.0.3
   │     ├─ @webassemblyjs/helper-code-frame@1.7.6
   │     │  └─ @webassemblyjs/wast-printer@1.7.6
 ・
 ・
 ・

と1000件以上の依存パッケージがある

というわけでpackage-lock.jsonは自動生成されるファイルだが(npm5以降自動生成されるようになった)、gitにcommitすることが推奨されている。

ダウンロードしてきた依存パッケージはどこに保存される?

さて、いま依存パッケージがたくさんあることをみてきたが、それらパッケージはnpm installされたあと、どこに保存されるのか。

現在のディレクトリ構成↓を再度みてみると、node_modulesというディレクトリも自動的に生成されている。

現在のディレクトリ構成

es6hello
├── node_modules
├── package.json
└── package-lock.json

このnode_modulesデイレクトリが、ダウンロードしたパッケージの保存先となる。

node_modulesはgit管理しない

node_modulesはダウンロードされてきた依存パッケージが保存されているが、これらはgit管理したくないので、以下のように.gitignoreファイルを作成してgit管理対象から外す。

.gitignore
/node_modules/

<現在のディレクトリ構成>

es6hello
├── node_modules
├── .gitignore
├── package.json
└── package-lock.json

超ミニマムなwebアプリを記述する

さて、ここではブラウザで動作する一番シンプルなアプリを作る。

シンプルなhtmlとjsのソースコードを用意する

まず作業ディレクトリes6hello直下にブラウザで開く用の index.htmlを作成する

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Example</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>

このindex.htmlはbodyに

<script src="main.js"></script>

を入れただけの非常にシンプルなもの。

次に、jsのソースコードを格納するためのsrcディレクトリを作り、そこにindex.jsという名前のファイルを作る。

index.js
alert("hello");

index.htmlとsrc/index.jsを加えたので
ディレクトリ構成は↓のようになる

es6hello
├── node_modules
├── src
│   └── index.js
├── .gitignore
├── index.html
├── package.json
└── package-lock.json

webサーバーを使えるようにする

webpackにはwebpack-dev-serverという開発用のwebサーバーが準備されている。
このサーバーを起動するためにpackage.jsonのscripts以下に

"start": "webpack-dev-server",

を追加する。

package.jsonの全体は以下のようになる。

package.json
{
  "name": "es6hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Tom Misawa <riversun.org@gmail.com> (https://github.com/riversun)",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.19.1",
    "webpack-cli": "^3.1.0",
    "webpack-dev-server": "^3.1.8"
  }
}

"start": "webpack-dev-server"のように記述すると
コマンドラインでnpm run startと入力したときに、webpack-dev-serverというコマンドが実行される。

webアプリの実行

以下を入力するとwebアプリを実行できる

コマンドライン
npm run start

と入力すると以下のように、webサーバーが起動してアプリを試すことができるようになる。

> es6hello@1.0.0 start dev/es6hello
> webpack-dev-server

i 「wds」: Project is running at http://localhost:8080/
i 「wds」: webpack output is served from /
? 「wdm」: Hash: 6a10213e244bb78370d8
Version: webpack 4.19.1
Time: 949ms
Built at: 2018-10-01 12:41:23
  Asset     Size  Chunks             Chunk Names
main.js  139 KiB       0  [emitted]  main
Entrypoint main = main.js
 [2] multi (webpack)-dev-server/client?http://localhost:8080 ./src 40 bytes {0} [built]
 [3] (webpack)-dev-server/client?http://localhost:8080 7.78 KiB {0} [built]
 [4] ./node_modules/url/url.js 22.8 KiB {0} [built]
 [7] ./node_modules/url/util.js 314 bytes {0} [built]
 [8] ./node_modules/querystring-es3/index.js 127 bytes {0} [built]
[11] (webpack)-dev-server/node_modules/strip-ansi/index.js 161 bytes {0} [built]
[12] (webpack)-dev-server/node_modules/ansi-regex/index.js 135 bytes {0} [built]
[13] ./node_modules/loglevel/lib/loglevel.js 7.68 KiB {0} [built]
[14] (webpack)-dev-server/client/socket.js 1.05 KiB {0} [built]
[15] ./node_modules/sockjs-client/dist/sockjs.js 177 KiB {0} [built]
[16] (webpack)-dev-server/client/overlay.js 3.58 KiB {0} [built]
[18] ./node_modules/html-entities/index.js 231 bytes {0} [built]
[21] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {0} [built]
[23] (webpack)/hot/emitter.js 75 bytes {0} [built]
[25] ./src/index.js 15 bytes {0} [built]
    + 11 hidden modules

この状態で、http://localhost:8080 にアクセスすると以下のようになる。

image.png

alertでダイアログを出すだけだが、これでもっともシンプルなwebアプリができた。

★ここまでの全ソースコードはこちら

さて、npm run startで起動されたWebサーバーはwebpack-dev-serverといい、コマンドラインでwebpack-dev-serverと入力するだけでも起動できる。

この webpack-dev-server の起動条件(起動パラメータ)は細かく定義できるが、いまはとくに何も設定していない。つまりデフォルト動作となる。

webpack-dev-serverのデフォルト動作 は以下のとおり。

エントリポイント ./src/index.js
コンテンツのルート(content-base) / (ワークディレクトリの直下)
ホスト http://localhost:8080
バンドル http://localhost:8080/main.js

npm runとnpm start

そもそもnpm runとは何か。
npm runとは以下のような構文をとる。

npm run <command>

command部分は、package.jsonのscript以下に定義する。

package.json(抜粋)
 "scripts": {
    "start": "webpack-dev-server",

この場合は command部分はキーが"start"で実行したいコマンドは "webpack-dev-server"となる。

仮に以下のような定義をしていた場合には

package.json(抜粋)
 "scripts": {
    "dev": "webpack-dev-server",

これを実行する場合は npm run devで実行することができる。

npm run dev

ちなみにnpm runnpm run-scriptのエイリアス(つまり別名)なのでnpm run-scriptとしてもOK。通常npm runのほうが短いのでそっちを使う人が多い。

よく使うコマンドは特別扱いされていて、"start""stop""restart""test"などは、

npm start

のようにrunをつけずに使うことができる。

つまり npm run-script startnpm run start とできて、さらに npm start でもOKとなる。
今後は npm start を使っていく。

クラスを定義する

ES6からはクラスが使えるようになったので、クラスを使ってコードを書いていく。

以下のように./src/hello.jsを作成し、あいさつをするクラスを作ってそれをブラウザで実行できるようにしていく。

hello.js
export default class Hello {

    //コンストラクタ
    constructor() {
    }

    /**
     * 挨拶をする
     * @returns {string}
     */
    sayHello() {
        const hello = 'Hi, there!';
        console.log(hello);
        return hello;
    }
}

このクラスはsayHelloメソッドを呼び出すと、"Hi,there!"と挨拶するだけのシンプルなプログラム。

exportとは何か?

exportとは、このモジュールにあるクラスや変数を外部のファイルからも使えるよ、という宣言のこと。
外部のファイル側からは import文を使うことで利用できるようになる。

export defalut とは何か

export defalutとは何か。

export default class Hello 

あるモジュール(jsファイル)には複数の変数や関数やクラスを入れることができ、それぞれの変数や関数やクラスは、それぞれ個別にexportすることができる。

export default classのようにdefaultをつけると、複数のうちこのクラスがメインの処理になりますよ、という風に特別視することができる。
defaultはモジュールにつき1つだけしか宣言できない。

今後、クラスを使う場合は export default classのみを使っていく。

export でdefaultを使うか使わないかはimport側での取り扱いが変わってくるのでそちらでみる。

さっそく exportしたクラスをimportしてみる

ということで、いまexportしたクラスをエントリポイントである./src/index.jsでインポートして使ってみる。

さきほど最初につくったindex.js内容を消して以下のようにする

index.js
import Greeting from './hello.js';

const greeting = new Greeting();
alert(greeting.sayHello());

import文は

import Greeting from './hello.js';

となる。これは hello.js にある default で宣言されたクラスを 「Greeting という名前で参照するよ」という宣言となる。ここではあえて参照名を Greetingにしたが、つまり、もとのhello.jsで定義されていた Hello という名前と同一のものである必要は無いということ。

import以降このクラスは Greeting という名前で使うことができるので、
このクラスを new するには以下のようになる。

const greeting = new Greeting();

※ 参考:importのバリエーションはこちら

起動してみる

npm start

npm start をして http://localhost:8080 にアクセスすると、以下のようになる。

image.png

無事実行できている。
(実際にはJavaScriptをES6の記法のまま実行しているので古いブラウザだと動作しない場合がある、その為の対策=babelは後で説明する)

ちなみに、コンソールのほうにも出力されている。

image.png

★ここまで(「クラスを定義する」)の全ソースコードはこちら

現在のディレクトリ構成は以下のとおり。

es6hello
├── node_modules
├── src
│   ├── hello.js
│   └── index.js
├── .gitignore
├── index.html
├── package.json
└── package-lock.json

jsのコードをブラウザから呼び出す場合は、

<script src="main.js"></script>

のようにする。
いままでに作った index.jsとhello.jsはどうなったかというと、それらは main.jsとして1つにマージされ、ブラウザから main.js として呼び出すことができる。

では、index.jsとhello.jsは、いつマージされるのか

こたえ、ブラウザからのリクエスト時となる。

ブラウザからの呼び出しに応答するのはwebサーバーでもあるwebpack-dev-serverだが、呼び出し時にwebpack-dev-server経由でindex.jsとhello.jsがマージされ、main.jsとしてアクセスできるようになる。

webpack-dev-serverでホストされているように見せかけられるmain.jsは、ブラウザからアクセスすることができるが、ファイルとしての実体は無くメモリ上に存在する

webpack-dev-server を便利に設定する

これまでwebpack-dev-serverは特にカスタマイズせずデフォルト動作でつかってきたが、より便利に活用するためにいくつか設定をしていく。

webpack.config.js とは何か

webpack.config.jsは、webpackやwebpack-dev-serverの挙動を決めるための設定ファイルとなる。
早速、webpack-dev-serverの挙動を設定したいのでルートディレクトリにwebpack.config.jsファイルを作成する。

現在のディレクトリ構成

es6hello
├── node_modules
├── src
│   ├── hello.js
│   └── index.js
├── .gitignore
├── index.html
├── package.json
├── package-lock.json
└── webpack.config.js

webpack.config.jsを編集する

webpack.config.jsの中身は以下のようにする。

webpack.config.js
const path = require("path");

module.exports = {
    mode: 'development',
    devServer: {
        open: true,
        openPage: "index.html",
        contentBase: path.join(__dirname, "public"),
        watchContentBase: true,
        port: 8080,
    }
};

const path = require("path");

ここでrequireしているpathはNode.jsの標準モジュールで、ファイルやディレクトリのパス関連のユーティリティを提供しているモジュール。

下にでてくる

  contentBase: path.join(__dirname, ''),

で使われる。(説明はcontentBaseのところで)

 mode: 'development',

mode=モードの指定はwebpack4以降に必要となっている。指定しないと警告が出る。
モードは、webpackによるコードの最適化に関する指定で、"development"と"production"(デフォルト)がある。

今は開発中なので最適化など不要のため "development" を指定する。

webpack-dev-server関連の設定

webpack.config.jsのdevServer以下は、webpack-dev-server用の設定となる。
早速みていく。

webpack.config.jsの抜粋
  devServer: {
        open: true,
        openPage: "index.html",
        contentBase: path.join(__dirname, "public"),
        watchContentBase: true,
        port: 8080,
    }

上からみていくと、

open: true,
openPage: "index.html",

open:trueは、webpack-dev-server起動時(npm startなどで)に自動的にブラウザを起動する。
openPage: "index.html"は、自動的にブラウザを起動するときに開くページを指定している。

contentBase: path.join(__dirname, "public"),

contentBaseには、htmlファイルや画像、CSSなどのコンテンツのルートディレクトリを指定する。

ここで指定しているpath.join(__dirname, "public")をみていく。

__dirnameには、現在のモジュールのディレクトリ名が格納される。つまり、es6helloディレクトリが絶対パスで格納される。

path.joinは複数のパスを区切り文字で連結する関数で、
path.join("/temp/es6hello","bar")と指定した場合は /temp/es6hello/bar を返す。
それなら、path.join(__dirname, "public")___dirname+"/public" でいいじゃん、と思うかもしれないが、 path.join を使うことによってプラットフォーム間(例えば windowsとlinux)の区切り文字の違いを吸収してくれるので path.join を使っておいた方が良い。

ということで、contentBase: path.join(__dirname, "public")によって、es6hello作業ディレクトリ以下publicというディレクトリがコンテンツのルートディレクトリという設定をした。

そこで、publicというディレクトリを作り、そこに index.htmlを移動させる。
ちなみにwebpack.config.jsを作る前は、webpack-dev-serverのデフォルトの挙動で contentBaseは/(ルートディレクトリ直下)に設定されていた。

 watchContentBase: true,

watchContentBaseは、いま設定した contentBase 以下にあるファイルに変更があった場合に自動的にブラウザをリロードする機能の設定となり、これをtrueに設定すると、有効になる。

これにより index.html などを編集して保存すると即座にブラウザがリロードされる。

ちなみに、jsのコードのほうはデフォルトでオートリフレッシュ機能が有効となっているので、これでjsファイルもhtmlも編集したらブラウザのリロードが自動で動作するようになった。

コマンドライン
npm start

npm startでブラウザが自動的に起動し、アプリが実行される。

★ここまで(「webpack-dev-server を便利に設定する」)の全ソースコードはこちら

現在のディレクトリ構成

es6hello
├── node_modules
├── public
│   └── index.html
├── src
│   ├── hello.js
│   └── index.js
├── .gitignore
├── package.json
├── package-lock.json
└── webpack.config.js

Helloクラスを外部から参照する

いまは、index.jsの中に import 文を書いて Hello クラスを参照しているが、index.html内に書いた<script>内から直接 Hello クラスを参照できるようにしたい。

(1)index.htmlの編集

その為に、まず index.htmlを編集する。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Example</title>
</head>
<body>
<script src="main.js"></script>
<script>
const greeting = new Greeting();
alert(greeting.sayHello());
</script>
</body>
</html>

追加した部分は以下となる。

<script>
const greeting = new Greeting();
alert(greeting.sayHello());
</script>

つまり、直接、index.htmlの中に<script>タグを追加して、Greetingクラスをnewする処理を記述している。

(さきほども書いたが、JavaScriptのこの記法は ES6(ECMAScript6) なので、古いブラウザでは動作しないかもしれない。これに対しては、後で対策をする。最新のChromeなら動作する)

(2)index.js の編集

index.htmlを修正したが、このままでは動かない。
なぜなら、 Greeting というクラスが外に公開されていないため、外からはアクセスできない。
そこで、index.jsを以下のようにする。

index.js(変更前)
import Greeting from './hello.js';

const greeting = new Greeting();
alert(greeting.sayHello());
index.js(変更後)
export { default as Greeting }  from './hello.js';

これは、hello.jsにあるdefault、つまりexport default class Helloと定義されたクラスをGreetingという名前でエクスポートしますよ、という宣言となる。

(3)webpack.config.jsの編集

さらに、index.html内の<script>タグからの参照のようにライブラリ的に使いたい場合は、webpack.config.jsを以下のように編集する。

webpack.config.js
const path = require("path");

module.exports = {
    mode: 'development',
    devServer: {
        open: true,
        openPage: "index.html",
        contentBase: path.join(__dirname, 'public'),
        watchContentBase: true,
        port: 8080,
    },
    output: {
        libraryTarget: 'umd'
    }
};

追加した部分は

    output: {
        libraryTarget: 'umd'
    }

libraryTarget: 'umd'をoutput以下に追加することで、ライブラリモードが有効になり、バンドル main.js にあるexportされたクラスや関数にアクセスできるようになる。
umdについては次の章で触れる。

実行

ここまでで実行すると

npm start

以下のように、うまくいった!

image.png

★ここまで(「Helloクラスを外部から参照する」)の全ソースコードはこちら

webpackを使ってライブラリを作る

ライブラリとは、ある機能(群)の入った独立したjsモジュール。要はjqueryみたなもので、ブラウザから使う場合には<script>タグの中に<script src="hogehoge.js></script>のように読み込んで使うことができるものを作る。

もちろん、npmモジュールにしてnpmリポジトリに登録しておけば、package.jsonに依存関係を書くだけで簡単につかえるようにすることも可能。

webpack.config.jsの編集

webpack.config.jsを以下のようにする

webpack.config.js
const path = require("path");

module.exports = {
    mode: 'development',
    devServer: {
        open: true,
        openPage: "index.html",
        contentBase: path.join(__dirname, 'public'),
        watchContentBase: true,
        port: 8080,
    },
    entry: {app: './src/index.js'},
    output: {
        publicPath: "/js/",
        filename: '[name].js',
        library: ["com", "example"],
        libraryTarget: 'umd'
    }
};

以下がポイントなので1つずつみていく。

webpack.config.jsの変更点
    entry: {app: './src/index.js'},
    output: {
        publicPath: "/js/",
        filename: '[name].js',
        library: ["com", "example"],
        libraryTarget: 'umd'
    }


エントリーをセットする
entry: {app: './src/index.js'},

エントリーとはそもそも何か、複数のjsファイルをimportしているファイルと考えておけばOK。
複数のエントリーを扱いときのため、↑のように、連想配列にして指定できる。この例では「appというキーに対して./src/index.jsがエントリーとしてセットされますよ」という意味となる。このペアを複数セットしておくと、複数のバンドルを生成することができる。今は複数不要だが、複数になってもいいようにこの記法で書いておく。


バンドルjsのファイル名をセットする
        filename: '[name].js',

生成されるバンドルの名前をセットする。
[name]というのはメタ記法で、[name]の中に、上のエントリーで書いたkey名が入るようになる。
上では{app: './src/index.js'}としているので、キーがappとなるので、バンドル生成時には app.js というファイルが生成される。webpack-dev-serverからアクセスする場合も同じ。


publicPathをセットする
       publicPath: "/js/",

ブラウザからバンドルにアクセスする際のjsのパスを指定する。
この例だと、ブラウザには<script src="js/app.js"></script>と記述する。つまりjs/というパスでアクセスできるようになるということ。


パッケージ名をセットする
  library: ["com", "example"],

これはいわゆるパッケージ名。配列に複数指定すると、
com.example のようにピリオドでつないだパッケージ名にすることができる。

パッケージ名は、以下のような形式でexportしたクラスをnewするときに使える。

const greeting = new com.example.Greeting();


libraryTargetでライブラリ化の方式を指定
  libraryTarget: 'umd'

umdとはUniversal Module Definitionのことで、ライブラリ化するときの方式を指定している。よほどの事情がなければ、umdを指定しておけばOK。
ほかにも amd(Asynchronous Module Definition)などが指定できるので、詳しくは、https://webpack.js.org/configuration/output/ にて。

index.htmlを編集

全体は以下の通り、

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Example</title>
</head>
<body>
<script src="js/app.js"></script>
<script>
const greeting = new com.example.Greeting();
alert(greeting.sayHello());
</script>
</body>
</html>

変更したのは、以下のjs/app.jsのところ。さきほど設定したpublicPathにあわせjsにしたところ。

<script src="js/app.js"></script>

もう1つの変更は

<script>
const greeting = new com.example.Greeting();
alert(greeting.sayHello());
</script>

com.example というパッケージ名でGreetingクラスにアクセスしている部分となる。

実行する

npm start

こちらも無事動作!

image.png

babel(バベル)を使ってすべてのブラウザで動作するようにする

ここまでいくらかJSコードを書いてきたが、これらのコードはすべて ES6 (ECMAScript 6)の記法で書いてきた。

ES6をサポートしていない古いブラウザも未だあるため、こうした古いjsランタイムを搭載したブラウザ向けのjsコードは古いjsコードつまりES5 (ECMAScript 5)のほうが無難である。

image.png

でも ES6 のクラスなどを使った方が見通しの良いコードがかけるなど ES6のメリットも多いため、コーディングするときは基本 新しいjs記法(ES7,ES6など) で書いて、ブラウザ向けには書いたコードを古いjsに変換して使う、ということが一般的に行われている。

babelというツールを使うと、ES6のコードをES5に自動的に変換できる。つまりコンパイルできる。(トランスパイルと言っているひともいる)

babelとは何か?

babelは(新しい記法の)jsコードの構造を解析して古いランタイムでも動作するようにコードを変換してくれるもの。

babelの使い方は簡単。何も難しいことは無いので1つずつ見ていく。

Babel v7をインストールする

以下により、最新のBabel v7をインストールする。

npm install --save-dev babel-loader @babel/core @babel/preset-env

save-dev すると、このプロジェクト用にbabelがインストールされる。
自動的にpackage.jsonのdevDependencies以下にbabel関連の依存関係が追加される

package.json(抜粋)
  "devDependencies": {
    "@babel/core": "^7.1.5",
    "@babel/preset-env": "^7.1.5",
    "babel-loader": "^8.0.4",
    "webpack": "^4.19.1",
    "webpack-cli": "^3.1.0",
    "webpack-dev-server": "^3.1.8"
  }

ここまでで、package.jsonは以下のようになっている。

package.json
{
  "name": "es6hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server",
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config webpack.config.js"
  },
  "author": "Tom Misawa <riversun.org@gmail.com> (https://github.com/riversun)",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.1.5",
    "@babel/preset-env": "^7.1.5",
    "babel-loader": "^8.0.4",
    "webpack": "^4.19.1",
    "webpack-cli": "^3.1.0",
    "webpack-dev-server": "^3.1.8"
  }
}

これでbabelが利用可能になった。

webpack4とBabel v7を連携できるようにする

webpack.config.jsに以下を追加する。

webpack.config.js
 module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            [
                                "@babel/preset-env",
                                {
                                    "useBuiltIns": "usage",
                                    "targets": "> 0.25%, not dead"
                                }
                            ]
                        ]
                    }
                }
            }
        ]
    },
    devtool: 'inline-source-map'

jsファイルがみつかった場合は babel-loader を呼び出して処理せよ、という意味になる。
options 以下にはbabel側に伝える挙動を記載する。

@babel/preset-envの意味

(webpack.config.jsから抜粋)
    presets: [
        [
            "@babel/preset-env",
            {
                "useBuiltIns": "usage",
                "targets": "> 0.25%, not dead"
            }
        ]
    ]

babelをつかって、古いブラウザでも動作するように変換したいが、preset-envでは何をサポートしたいのかを柔軟に指定することができる。

targets
ターゲットとなるブラウザ(やelectronなどのjsランタイムを利用した環境)を指定する。
この柔軟なターゲット指定こそbabelの特長でもあり、ありがたい機能でもある。
ここで、 "targets": "> 0.25%, not dead" の部分では、市場シェアが0.25%を超えるブラウザーで実行可能な最低限のコード出力せよ、という意味。

useBuiltIns
useBuiltInsは polyfill(ポリフィル=ブラウザが新しい機能に対応していない場合、それを補う為に古いブラウザでも動作する代替コードをあてがうこと)に関する設定をするためのもの。

"useBuiltIns": "usage" は自動的に必要なポリフィルをインポートしてくれる機能で、さらに各ファイルでポリフィルが必要な場合でもバンドルしたときにはポリフィルの読み込みが1回で済むように工夫してくれるオプションとなる。公式では experimental となっているので、この点を重視しないなら、"useBuiltIns": "false" でもOK。(デフォルトもfalseなので省略も可能)

targetを省略することも可能
ちなみに、以下のようにtargetを何も設定しない場合は、すべて強制的にES5に変換される(@babel/preset-es2015、@babel/preset-es2016、@babel/preset-es2017を同時に指定したのと同じ状態)が、せっかくの柔軟にターゲットを指定するbabelの能力を生かせないので推奨しないだそう。

babelでjsコードをとりあえずES5に変換する設定
{
  "presets": ["@babel/preset-env"]
}

@babel/preset-envのそのほか詳細は以下を参照
https://babeljs.io/docs/en/next/babel-preset-env.html

ソースマップを出力する

さて、そろそろ終盤に入ってきた。

webpack.config.jsに追加した以下の部分は、ソースマップの出力を有効にしている。

devtool: 'inline-source-map'

ブラウザ上でコードを実行するとバグがあってエラーが発生したときには、元のコードの行番号でスタックトレースが表示される。
今回はソースはES6記法で書くが、実際にブラウザ上で実行されるコードはbabelによって変換されたコードとなるため、もしエラーが発生してもこのままだと変換後のコードの行番号でスタックトレースが表示されてしまう。
ここで、上記のソースマップの設定をしておくと、ブラウザで実行するときにも、ソースコードの情報がきちんと埋め込まれる。

npm start

見た目ではわからないが、ES5のコードが出力された状態で無事実行された。

image.png

ここまでで webpack.config.jsは以下のようになっている。

webpack.config.js
const path = require("path");

module.exports = {
    mode: 'development',
    devServer: {
        open: true,
        openPage: "index.html",
        contentBase: path.join(__dirname, 'public'),
        watchContentBase: true,
        port: 8080,
    },
    entry: {app: './src/index.js'},
    output: {
        path: path.join(__dirname, "dist"),
        publicPath: "/js/",
        filename: '[name].js',
        library: ["com", "example"],
        libraryTarget: 'umd'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            [
                                "@babel/preset-env",
                                {
                                    "useBuiltIns": "usage",
                                    "targets": "> 0.25%, not dead"
                                }
                            ]
                        ]
                    }
                }
            }
        ]
    },
    devtool: 'inline-source-map'

};

(コラム)先頭に@(atmark)がついているパッケージは何か

ちなみに、@babel/coreのように先頭に@(atmark)がついているパッケージは何か。
違和感あり?
これは、スコープモジュール(scoped module)という。@babelにあたるところをスコープといい、ユーザー名をつけたり組織名をつけたりすることができる。
何でそんなことをしたいかというと、公開されているnpmパッケージの名前は早いモンがちで取得できるルールなので、たとえばbabel制作元でもない第三者が babel-xxxx みたいなnpmパッケージ名で登録することもできてしまう。
このように、パッケージ名のバッティングを防いただり、スコープとして組織名をつけて一連のパッケージ名の公式感を出すなどのメリットが期待できる。

詳しくは↓を参照
https://docs.npmjs.com/about-scopes

バンドルの出力先を指定する

さて、いままでは npm startでwebpack-dev-serverが起動するようにしていたので、
バンドルをファイルとして出力せず webpack-dev-serverのメモリ上に保持していた。

ここでは、バンドルをファイルとして出力してみる。

webpack.config.jsのoutputに以下の1行追加する

path: path.join(__dirname, "dist"),

すると↓のようになる。

wbpack.config.js(抜粋)
   output: {
        path: path.join(__dirname, "dist"),
        publicPath: "/js/",
        filename: '[name].js',
        library: ["com", "example"],
        libraryTarget: 'umd'
    },

path: path.join(__dirname, "dist") によって、バンドルjs、この場合は、app.js が[作業ディレクトリ]/dist/app.js として生成される。

次に、実際にバンドルを生成するためのコマンドをpackage.jsonで設定する
package.jsonのscriptsに

   "build": "webpack --config webpack.config.js"

の1行を追加してscript以下を↓のようにする。

package.json
 "scripts": {
    "start": "webpack-dev-server",
    "build": "webpack --config webpack.config.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

これでpackage.jsonの全体は以下のようになった

package.json
{
  "name": "es6hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server",
    "build": "webpack --config webpack.config.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Tom Misawa <riversun.org@gmail.com> (https://github.com/riversun)",
  "license": "ISC",

  "devDependencies": {
    "@babel/core": "^7.1.5",
    "@babel/preset-env": "^7.1.5",
    "babel-loader": "^8.0.4",
    "webpack": "^4.19.1",
    "webpack-cli": "^3.1.0",
    "webpack-dev-server": "^3.1.8"
  }
}

バンドルjsを生成する

コマンドラインで npm run buildを実行する

npm run build

> es6hello@1.0.0 build dev/es6hello
> webpack --config webpack.config.js

Hash: 4c781264bbf44ceb06b4
Version: webpack 4.19.1
Time: 562ms
Built at: 2018-11-10 20:55:40
 Asset      Size  Chunks             Chunk Names
app.js  12.6 KiB     app  [emitted]  app
Entrypoint app = app.js
[./src/hello.js] 1.12 KiB {app} [built]
[./src/index.js] 49 bytes {app} [built]

実行すると、distディレクトリに app.js が生成された。

ここまでのディレクトリ構成

es6hello
├── dist
│   └── app.js
├── node_modules
├── public
│   └── index.html
├── src
│   ├── hello.js
│   └── index.js
├── .gitignore
├── package.json
├── package-lock.json
└── webpack.config.js

★ここまで(「babel(バベル)を使ってすべてのブラウザで動作するようにする」)の全ソースコードはこちら

ライブラリをnpmモジュールとして公開する

公開するために必要な情報を package.jsonに追記する

さて、これまで作ってきたライブラリをnpmモジュールとして公開する準備にとりかかる。

リポジトリの情報やホームページの情報を含む以下のデータをpackage.jsonに挿入する。

  "repository": {
    "type": "git",
    "url": "git+https://github.com/riversun/es6hello.git"
  },
  "bugs": {
    "url": "https://github.com/riversun/es6hello/issues"
  },
  "homepage": "https://github.com/riversun/es6hello#readme",

上記をふくめたpackage.jsonの全体は↓のようになる。いよいよこれが完成版!となる。

package.json
{
  "name": "es6hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server",
    "build": "webpack --config webpack.config.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Tom Misawa <riversun.org@gmail.com> (https://github.com/riversun)",
  "license": "ISC",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/riversun/es6hello.git"
  },
  "bugs": {
    "url": "https://github.com/riversun/es6hello/issues"
  },
  "homepage": "https://github.com/riversun/es6hello#readme",
  "devDependencies": {
    "@babel/core": "^7.1.5",
    "@babel/preset-env": "^7.1.5",
    "babel-loader": "^8.0.4",
    "webpack": "^4.19.1",
    "webpack-cli": "^3.1.0",
    "webpack-dev-server": "^3.1.8"
  }
}

作ってきたすべてのファイルをcommit/pushする

作ってきたファイルをすべてcommitして、
最初に作った GitHubにpushする。

git push

このようにGitHubにプロジェクトを公開しておくことで、ライブラリがnpmとして公開されたとき、GitHubのreadmeを取り込んだ説明ページや、GitHubへのリンクが、npmモジュールのカバーページとしてnpmjs.com側で自動生成される。

npmjs.comにサインアップする

npmモジュールを公開するにはnpmjs.comにサインアップする必要がある。

https://www.npmjs.com/
にて、画面に従いサインアップする(サインアップがまだの場合)

image.png

npmにログインする

サインアップしたら、以下のようにコマンドラインからnpm loginをして、npmjs.comで登録した username、passwordでログインする。

npm login
Username: riversun
Password:
Email: (this IS public) riversun.org@gmail.com
Logged in as riversun on https://registry.npmjs.org/.

npm publishする

npm publishコマンドで、今作ったライブラリをnpmモジュールとして公開することができる。

npm publish

+ es6hello@1.0.0

これで今作った 挨拶クラス がnpmモジュールとしてライブラリ登録され公開された♪

https://www.npmjs.com/package/es6hello

image.png

まとめ

今風のjs開発の知識がゼロ状態から、Node.jsの環境、npm、webpack4、babelなど使いハンズオンでnpmモジュールを作るところまで説明してきました。

このプロジェクトは https://github.com/riversun/es6hello に公開しています。
jsフロントエンドプロジェクトのひな形としても活用可能です。

  • 準備
  • 最後にnpmモジュールとして公開したいから、npmプロジェクトをGit管理下におき、GitHubにpushする
  • webpackを使えるようにする
  • 超ミニマムなwebアプリを記述する
  • クラスを定義する
  • webpack-dev-server を便利に設定する
  • webpackを使ってライブラリを作る
  • babel(バベル)を使ってすべてのブラウザで動作するようにする
  • ライブラリをnpmモジュールとして公開する
  • まとめ