新人にドヤ顔で説明できるか、今風フロントエンド開発ハンズオン(git/node.js/ES6/webpack4/babel7)
新人にドヤ顔で説明できるか、今風フロントエンド開発ハンズオン(git/node.js/ES6/webpack4/babel7):
本投稿では、以下のようなキーワードが出てきます。
node.js、npm、ES6(ECMAScript6)、webpack4、babel v7、ソースマップ、クラス、jsでオブジェクト指向
この記事のソースコード(最終版)は https://github.com/riversun/es6hello.git にあります。
クローンしてサンプルを実行する場合は、以下のようにします。
node/npmをそれぞれの環境にあわせてインストールしておく
私のバージョンは以下のとおり。
まずプロジェクトの作業ディレクトリを作る
任意の名前をつける。ここでは es6helloとする
npm initをして、このディレクトリをnpmプロジェクトにする
いろいろきかれるが、後から変更できるのでここではとりあえずすべてエンターでOK。
(エンターを9回程度押す)
これでディレクトリに package.json が生成される
これでnpmプロジェクト完成!
現在のディレクトリ構成
なぜgitの部分まで説明しているかというと、最終的に作ったjsを、外部から使えるnpmモジュールとして公開することをもくろんでいるのでgit管理の説明をする。
npmモジュールにすることに全く興味がなければ、gitの部分はすべて読み飛ばしてOK。
ソースコードのバージョン管理をするためにディレクトリをgit管理下に置く。
リモート側はgithubを使う
このディレクトリをgit管理下におく
このディレクトリ以下のすべてのファイルをgit管理下におく
(といっても、まだpackage.jsonしかない)
ちなみに、
package.jsonをコミットする
ついでにgitのリモートリポジトリにpushするところまでやっておく
githubにリポジトリができたらURLをコピーする
リモート名:originにgithubのリポジトリをひもづける
これでリモート originにgithubにつくったリポジトリがひもづいた。
現在のブランチはデフォルト設定なので
現在のブランチをリモート側(origin)のmasterブランチにpushする
(pushするときにupstreamブランチを指定する)
以下で、現在のブランチがどのupstreamブランチを追跡するか確認する
upstreamブランチが設定できていることが確認できた。
また、github側にもちゃんとpushできていることを確認。
以降は
これでひとまずローカル側とリモート側にgitの環境が整った。
早くjavascriptでコードを書きたいところだが、まずは最低限の環境をつくるところからはじめる。
まずはwebpack導入から。
webpackとはモジュールバンドラーと呼ばれるモノで、要は複数のjsファイルを良い感じに処理して1つにまとめてくれるツールのこと。
大規模なソフトを開発する場合には、ソースコードを複数のファイルに分割して開発する。
分割することをモジュール化といったりする、また分割された1つ1つのファイルをモジュールと呼んだりする。
以前は、下のようにブラウザからjsファイルを読み込むときに複数のjsファイルをscriptタグで列挙して読み込むということが割と一般的だった。
読み込むモジュールが3つくらいなら、これも特に問題はないが、これが増えてくるとどのモジュールとどのモジュールの依存関係があるか、どのモジュールを先に読み込むか、など問題がでてくる。
またnpmで外部公開されているパッケージを取り込むときにもかなり面倒になってくる。
node.jsのサーバーサイド向け仕様では、こうした問題に対応するためにrequireで依存モジュールを解決したり、npmで外部パッケージを取り込んで利用したりが簡単にできるようになった。
JavaScriptも他の言語プラットフォーム(JavaやRubyなど)同様の開発エコシステムが急速に(※)整備された。
(※私がjsに初めて触れたのは1990年代後半なので、急速に整備されたように感じるw)
そうした便利な機能をフロントエンドつまりブラウザ用のJavaScript開発でも使えるようにしてくれるのがwebpackとなる。(もちろんnpm等も仕組みもあわせてつかう)
いちばんシンプルにwebpackをとらえるなら以下のように複数のjsを1つにまとめてくれる、機能となる。
webpackと同じようなことをしてくれるツールはいくつかある(以前流行っていたbrowserifyや最近でてきたparcelなど)が現時点でいちばんメジャーなのでこちらを使う。最新のバージョンは webpack4系となる。
早速webpackをインストールしていく。
コマンドプロンプトに
するとインターネットから3つのパッケージ(webpack webpack-cli webpack-dev-server)が自動的にダウンロードされる。
どれもwebpack関連のパッケージとなる。
しばらく待つと、webpackのインストールが終了する。
現在のディレクトリ構成
node_modulesというディレクトリとpackage-lock.jsonというファイルが生成された模様。
これは後で説明する。
webpackのインストールが終わると、package.jsonには以下が自動的に追加される。
npm installコマンドはnpmのパッケージをインストールしてね、という意味、
そのパッケージは、--save-devは開発時のみに使うという意味となる。
npm install --save-dev パッケージ名 パッケージ名 パッケージ名のように複数のパッケージを同時に指定することができる。
上のpackage.jsonの追加分にあったdevDependencies以下には、このプロジェクトで利用するパッケージが記述される。プロジェクトがこのパッケージに依存しているので依存パッケージとも呼ぶ。
さきほどのパッケージインストール時には --save-devを指定して開発時のみにつかうことを指定したが、それがpackage.jsonではdevDependenciesとなる。
devDependencies以下にある "webpack": "^4.19.0"は
バージョンのところにある「^」(キャレット)に注目。「おいおい、^は何だよ」っていう話だが、これはキャレット表記と呼びバージョン指定をするときに特別な意味をもっている。
さて、このキャレット表記の意味だが、^4.19.0は、パッケージのバージョンXが 4.19.0≦ X<5.0.0の範囲ならOKという意味になる。(こちら https://docs.npmjs.com/misc/semver に詳しい説明がある)
それだけでは意味がよくわからないので、まずこのバージョン表記の意味からみていく。
npmパッケージのバージョン、たとえば4.19.0というバージョン名の付け方はセマンティックバージョニングというルールに従っている。
↓のように、それぞれの数字の意味が メジャーバージョン.マイナーバージョン.パッチバージョンの意味をもつ。
ちなみに、後方互換とはAPIのバージョンが上がっても、旧APIと互換性が保たれるという意味。
さきほどにもどると、キャレット表記つまり^4.19.0が4.19.0≦ X<5.0.0の範囲ということで「メジャーバージョンは固定するよ、ただマイナーバージョンやパッチバージョンは変化するよ(上がる)」という意味になる。
メジャーバージョンが固定されるので、パッケージを作った人がセマンティックバージョニングの精神にきちんと従っていれば理屈上は後方互換が担保される。
ただし、これは、あくまでも理屈。
人間がやることなのでマイナーバージョンやパッチバージョンを上げるような更新の場合でもうっかり忘れで後方互換が破綻する場合もある。
そのあたりの課題を解決するために考えられたのが次にでてくる package-lock.jsonの話につながる。
もういち現在は↓のようなディレクトリ構成になっており、package-lock.jsonが自動的に追加されている。
現在のディレクトリ構成
さきほどキャレット表記でメジャーバージョンを固定することで後方互換が理屈上は担保されていたが、実際には、パッチバージョンを変えたら動かなくなってしまった、みたいなことが起こっている。
また、ある依存パッケージが別の依存パッケージに依存しているいわゆるツリー上(入れ子上)の依存関係になっていることが良くあるため、ツリーのどこかにある依存パッケージのマイナーバージョンやパッチバージョンがあがったりすると、イッキに動かなくなってしまうことが起こりえる。
なので、やっぱり依存パッケージのバージョン番号は メジャー・マイナー・パッチ すべて固定(ロック)しようよ、というのがpackage-lock.jsonのお役目となる。
最上位のパッケージだけ固定しても仕方がないので、依存パッケージの依存パッケージといったツリー上の依存関係にある全パッケージのバージョン情報がpackage-lock.jsonに書き出される。
例えば、webpack 4.19.1 の依存パッケージのツリーは以下のようになる。ツリーをたどっていくとなんと1000個以上の依存パッケージがあり、深くネストしている。
ここでは参考までに
(依存関係をグラフィカルに表示してくれるサイトもある。)
と1000件以上の依存パッケージがある
というわけでpackage-lock.jsonは自動生成されるファイルだが(npm5以降自動生成されるようになった)、gitにcommitすることが推奨されている。
さて、いま依存パッケージがたくさんあることをみてきたが、それらパッケージはnpm installされたあと、どこに保存されるのか。
現在のディレクトリ構成↓を再度みてみると、node_modulesというディレクトリも自動的に生成されている。
現在のディレクトリ構成
このnode_modulesデイレクトリが、ダウンロードしたパッケージの保存先となる。
node_modulesはダウンロードされてきた依存パッケージが保存されているが、これらはgit管理したくないので、以下のように.gitignoreファイルを作成してgit管理対象から外す。
<現在のディレクトリ構成>
さて、ここではブラウザで動作する一番シンプルなアプリを作る。
まず作業ディレクトリes6hello直下にブラウザで開く用の index.htmlを作成する
このindex.htmlはbodyに
を入れただけの非常にシンプルなもの。
次に、jsのソースコードを格納するためのsrcディレクトリを作り、そこにindex.jsという名前のファイルを作る。
index.htmlとsrc/index.jsを加えたので
ディレクトリ構成は↓のようになる
webpackにはwebpack-dev-serverという開発用のwebサーバーが準備されている。
このサーバーを起動するためにpackage.jsonのscripts以下に
を追加する。
package.jsonの全体は以下のようになる。
コマンドプロンプトでnpm run startと入力したときに、webpack-dev-serverというコマンドが実行される。
以下を入力するとwebアプリを実行できる
と入力すると以下のように、webサーバーが起動してアプリを試すことができるようになる。
この状態で、http://localhost:8080 にアクセスすると以下のようになる。
alertでダイアログを出すだけだが、これでもっともシンプルなwebアプリができた。
★ここまでの全ソースコードはこちら★
さて、npm run startで起動されたWebサーバーはwebpack-dev-serverといい、コマンドプロンプトでwebpack-dev-serverと入力するだけでも起動できる。
この webpack-dev-server の起動条件(起動パラメータ)細かく定義できるが、いまはとくに何も設定していない。つまりデフォルト動作となる。
webpack-dev-serverのデフォルト動作 は以下のとおり。
そもそもnpm runとは何か。
npm runとは以下のような構文をとる。
command部分は、package.jsonのscript以下に定義する。
この場合は command部分はキーが"start"で実行したいコマンドは "webpack-dev-server"となる。
仮に以下のような定義をしていた場合には
これを実行する場合は npm run devで実行することができる。
ちなみにnpm runはnpm run-scriptのエイリアス(つまり別名)なのでnpm run-scriptとしてもOK。通常npm runのほうが短いのでそっちを使う人が多い。
よく使うコマンドは特別扱いされていて、"start"、stop、restart、testなどは、
のようにrunをつけずに使うことができる。
つまり npm run-script start は npm run start とできて、さらに npm start でもOKとなる。
今後は npm start を使っていく。
ES6からはクラスが使えるようになったので、クラスを使ってコードを書いていく。
以下のように./src/hello.jsを作成し、あいさつをするクラスを作ってそれをブラウザで実行できるようにしていく。
このクラスはsayHelloメソッドを呼び出すと、"Hi,there!"と挨拶するだけのシンプルなプログラムです。
exportとは、このモジュールにあるクラスや変数を外部のファイルからも使えるよ、という宣言のこと。
外部のファイル側からは import文を使うことで利用できるようになる。
export defalutとは何か。
あるモジュール(jsファイル)には複数の変数や関数やクラスを入れることができ、それぞれの変数や関数やクラスは、それぞれ個別にexportすることができる。
export default classのようにdefaultをつけると、複数のうちこのクラスがメインの処理になりますよ、という風に特別視することができる。
defaultはモジュールにつき1つだけしか宣言できない。
今後、クラスを使う場合は export default classのみを使っていく。
export でdefaultを使うか使わないかはimport側での取り扱いが変わってくるのでそちらでみる。
ということで、いまexportしたクラスをエントリポイントである./src/index.jsでインポートして使ってみる。
さきほど最初につくったindex.js内容を消して以下のようにする
import文は
となる。これは hello.js にある default で宣言されたクラスを 「Greeting という名前で参照するよ」という宣言となる。ここではあえて参照名を Greetingにしたが、つまり、もとのhello.jsで定義されていた Hello という名前と同一のものである必要は無いということ。
import以降このクラスは Greeting という名前で使うことができるので、
このクラスを new するには以下のようになる。
※ 参考:importのバリエーションはこちら
npm start をして http://localhost:8080 にアクセスすると、以下のようになる。
無事実行できている。
(だが、実際にはJavascriptをES6の記法のまま実行しているのでブラウザによっては動作しない場合がある、その為の対策=babelは後で説明する)
ちなみに、コンソールのほうにも出力されている。
★ここまで(「クラスを定義する」)の全ソースコードはこちら★
現在のディレクトリ構成は以下のとおり。
jsのコードをブラウザから呼び出す場合は、
のようにする。
いままでに作った 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.config.jsは、webpackやwebpack-dev-serverの挙動を決めるための設定ファイルとなる。
早速、webpack-dev-serverの挙動を設定したいのでルートディレクトリにwebpack.config.jsファイルを作成する。
現在のディレクトリ構成
webpack.config.jsの中身は以下のようにする。
ここでrequireしているpathはnode.jsの標準モジュールで、ファイルやディレクトリのパス関連のユーティリティを提供しているモジュール。
下にでてくる
で使われる。(説明はcontentBaseのところで)
mode=モードの指定はwebpack4以降に必要となっている。指定しないと警告が出る。
モードは、webpackによるコードの最適化に関する指定で、"development"と"production"(デフォルト)がある。
今は開発中なので最適化など不要のため "development" を指定する。
webpack.config.jsのdevServer以下は、webpack-dev-server用の設定となる。
早速みていく。
上からみていくと、
open:trueは、webpack-dev-server起動時(npm startなどで)に自動的にブラウザを起動する。
openPage: "index.html"は、自動的にブラウザを起動するときに開くページを指定している。
contentBaseには、htmlファイルや画像、CSSなどのコンテンツのルートディレクトリを指定する。
ここで指定しているpath.join(__dirname, "public")をみていく。
__dirnameには、現在のモジュールのディレクトリ名が格納される。つまり、es6helloディレクトリが絶対パスで格納される。
path.joinは複数のパスを区切り文字で連結する関数で、
それなら、
ということで、
そこで、publicというディレクトリを作り、そこに index.htmlを移動させる。
ちなみにwebpack.config.jsを作る前は、webpack-dev-serverのデフォルトの挙動で contentBaseは/(ルートディレクトリ直下)に設定されていた。
watchContentBaseは、いま設定した contentBase 以下にあるファイルに変更があった場合に自動的にブラウザをリロードする機能の設定となり、これをtrueに設定すると、有効になる。
これにより index.html などを編集して保存すると即座にブラウザがリロードされる。
ちなみに、jsのコードのほうはデフォルトでオートリフレッシュ機能が有効となっているので、これでjsファイルもhtmlも編集したらブラウザのリロードが自動で動作するようになった。
npm startでブラウザが自動的に起動し、アプリが実行される。
★ここまで(「webpack-dev-server を便利に設定する」)の全ソースコードはこちら★
現在のディレクトリ構成
いまは、index.jsの中に import 文を書いて Hello クラスを参照しているが、index.html内に書いた
その為に、まず index.htmlを編集する。
追加した部分は以下となる。
つまり、直接、index.htmlの中に
(ちなみに、JavaScriptのこの記法は ES6(ECMAScript6) なので、古いブラウザでは動作しないかもしれない。これに対しては、後で対策をする。最新のChromeなら動作する)
index.htmlを修正したが、このままでは動かない。
なぜなら、 Greeting というクラスが外に公開されていないため、外からはアクセスできない。
そこで、index.jsを以下のようにする。
これは、hello.jsにある
さらに、index.html内の
追加した部分は
libraryTarget: 'umd'をoutput以下に追加することで、ライブラリモードが有効になり、バンドル main.js にあるexportされたクラスや関数にアクセスできるようになる。
umdについては次の章で触れる。
ここまでで実行すると
以下のように、うまくいった!
★ここまで(「Helloクラスを外部から参照する」)の全ソースコードはこちら★
ライブラリとは、ある機能(群)の入った独立したjsモジュール。要はjqueryみたなもので、ブラウザから使う場合には
もちろん、npmモジュールにしてnpmリポジトリに登録しておけば、package.jsonに依存関係を書くだけで簡単につかえるようにすることも可能。
webpack.config.jsを以下のようにする
以下がポイントなので1つずつみていく。
エントリーとはそもそも何か、複数のjsファイルをimportしているファイルと考えておけばOK。
複数のエントリーを扱いときのため、↑のように、連想配列にして指定できる。この例では「appというキーに対して./src/index.jsがエントリーとしてセットされますよ」という意味となる。このペアを複数セットしておくと、複数のバンドルを生成することができる。今は複数不要だが、複数になってもいいようにこの記法で書いておく。
生成されるバンドルの名前をセットする。
[name]というのはメタ記法で、[name]の中に、上のエントリーで書いたkey名が入るようになる。
上では
ブラウザからバンドルにアクセスする際のjsのパスを指定する。
この例だと、ブラウザには
これはいわゆるパッケージ名。配列に複数指定すると、
**com.example*のようにピリオドでつないだパッケージ名にすることができる。
パッケージ名は、以下のような形式でexportしたクラスをnewするときに使える。
umdとはUniversal Module Definitionのことで、ライブラリ化するときの方式を指定している。よほどの事情がなければ、umdを指定しておけばOK。
ほかにも amd(Asynchronous Module Definition)などが指定できるので、詳しくは、https://webpack.js.org/configuration/output/ にて。
全体は以下の通り、
変更したのは、以下の
もう1つの変更は
こちらも無事動作!
ここまでいくらかJSコードを書いてきたが、これらのコードはすべて ES6 (ECMAScript 6)の記法で書いてきた。
ES6をサポートしていない古いブラウザも未だあるため、こうした古いjsランタイムを搭載したブラウザ向けのjsコードは古いjsコードつまりES5 (ECMAScript 5)のほうが無難である。
でも ES6 のクラスなどを使った方が見通しの良いコードがかけるなど ES6のメリットも多いため、コーディングするときは基本 新しいjs記法(ES7,ES6など) で書いて、ブラウザ向けには書いたコードを古いjsに変換して使う、ということが一般的に行われている。
babelというツールを使うと、ES6のコードをES5に自動的に変換できる。つまりコンパイルできる。(トランスパイルと言っているひともいる)
babelは(新しい記法の)jsコードの構造を解析して古いランタイムでも動作するようにコードを変換してくれるもの。
babelの使い方は簡単。何も難しいことは無いので1つずつ見ていく。
以下により、最新のbabel v7をインストールする。
自動的にpackage.jsonのdevDependencies以下にbabel関連の依存関係が追加される
ここまでで、package.jsonは以下のようになっている。
これでbabelが利用可能になった。
webpack.config.jsに以下を追加する。
jsファイルがみつかった場合は babel-loader を呼び出して処理せよ、という意味になる。
babelをつかって、古いブラウザでも動作するように変換したいが、preset-envでは何をサポートしたいのかを柔軟に指定することができる。
targets
ターゲットとなるブラウザ(やelectronなどのjsランタイムを利用した環境)を指定する。
この柔軟なターゲット指定こそbabelの特長でもあり、ありがたい機能でもある。
ここで、
useBuiltIns
useBuiltInsは polyfill(ポリフィル=ブラウザが新しい機能に対応していない場合、それを補う為に古いブラウザでも動作する代替コードをあてがうこと)に関する設定をするためのもの。
targetを省略することも可能
ちなみに、以下のようにtargetを何も設定しない場合は、すべて強制的にES5に変換される(@babel/preset-es2015、@babel/preset-es2016、@babel/preset-es2017を同時に指定したのと同じ状態)が、せっかくの柔軟にターゲットを指定するbabelの能力を生かせないので推奨しないだそう。
@babel/preset-envのそのほか詳細は以下を参照
https://babeljs.io/docs/en/next/babel-preset-env.html
さて、そろそろ終盤に入ってきた。
webpack.config.jsに追加した以下の部分は、ソースマップの出力を有効にしている。
ブラウザ上でコードを実行するとバグがあってエラーが発生したときには、元のコードの行番号でスタックトレースが表示される。
今回はソースはES6記法で書くが、実際にブラウザ上で実行されるコードはbabelによって変換されたコードとなるため、もしエラーが発生してもこのままだと変換後のコードの行番号でスタックトレースが表示されてしまう。
ここで、上記のソースマップの設定をしておくと、ブラウザで実行するときにも、ソースコードの情報がきちんと埋め込まれる。
見た目ではわからないが、ES5のコードが出力された状態で無事実行された。
ここまでで webpack.config.jsは以下のようになっている。
ちなみに、@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行追加する
すると↓のようになる。
次に、実際にバンドルを生成するためのコマンドをpackage.jsonで設定する
package.jsonのscriptsに
の1行を追加してscript以下を↓のようにする。
これでpackage.jsonの全体は以下のようになった
コマンドラインで npm run buildを実行する
実行すると、distディレクトリに app.js が生成された。
ここまでのディレクトリ構成
★ここまで(「babel(バベル)を使ってすべてのブラウザで動作するようにする」)の全ソースコードはこちら★
npmモジュールとして公開するために、リポジトリの情報やホームページの情報を含む以下のデータをpackage.jsonに挿入する。
すると、package.jsonの全体は↓のようになる。いよいよこれが完成版!となる。
作ってきたファイルを 最初に作った githubにpushする。
pushしておくと、npmとして公開されたとき、githubのreadmeを取り込んだ説明ページやgithubへのリンクが自動的にnpmjs.com側で作成される。
https://www.npmjs.com/
にて、画面に従いサインアップする(サインアップがまだの場合)
サインアップしたら、以下のようにコマンドラインからnpm loginをして、npmjs.comで登録した username、passwordでログインする。
npm publishコマンドで、今作ったライブラリをnpmモジュールとして公開することができる。
これで今作った 挨拶クラス がnpmモジュールとしてライブラリ登録され公開された♪
https://www.npmjs.com/package/es6hello
今風のjs開発の知識ゼロ状態から、node.jsの環境、npm、webpack4、babelなど使いハンズオンでnpmモジュールを作るところまで説明してきました。
このプロジェクトは https://github.com/riversun/es6hello に公開しています。
jsフロントエンドプロジェクトのひな形としても活用可能です。
概要
- 今風の手法で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)
(エンターを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
{ "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" }
現在のディレクトリ構成
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にリポジトリを作る
githubにリポジトリができたらURLをコピーする
ローカルgitのリモート側をgithubに設定する
リモート名:originにgithubのリポジトリをひもづけるgit remote add origin https://github.com/riversun/es6hello.git
現在のブランチをgithubにpushする
現在のブランチはデフォルト設定なので master
ブランチとなっているgit branch * master
(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.
git branch -vv * master 5cee35d [origin/master] first commit
また、github側にもちゃんとpushできていることを確認。
以降は
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>
またnpmで外部公開されているパッケージを取り込むときにもかなり面倒になってくる。
node.jsのサーバーサイド向け仕様では、こうした問題に対応するためにrequireで依存モジュールを解決したり、npmで外部パッケージを取り込んで利用したりが簡単にできるようになった。
JavaScriptも他の言語プラットフォーム(JavaやRubyなど)同様の開発エコシステムが急速に(※)整備された。
(※私がjsに初めて触れたのは1990年代後半なので、急速に整備されたように感じるw)
そうした便利な機能をフロントエンドつまりブラウザ用のJavaScript開発でも使えるようにしてくれるのがwebpackとなる。(もちろんnpm等も仕組みもあわせてつかう)
いちばんシンプルにwebpackをとらえるなら以下のように複数のjsを1つにまとめてくれる、機能となる。
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
どれもwebpack関連のパッケージとなる。
しばらく待つと、webpackのインストールが終了する。
現在のディレクトリ構成
es6hello ├── node_modules ├── package.json └── 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" }
"[パッケージ名]":"^[バージョン]"
の意味となる。バージョンのところにある「^」(キャレット)に注目。「おいおい、^は何だよ」っていう話だが、これはキャレット表記と呼びバージョン指定をするときに特別な意味をもっている。
さて、このキャレット表記の意味だが、^4.19.0は、パッケージのバージョンXが 4.19.0≦ X<5.0.0の範囲ならOKという意味になる。(こちら https://docs.npmjs.com/misc/semver に詳しい説明がある)
それだけでは意味がよくわからないので、まずこのバージョン表記の意味からみていく。
npmパッケージのバージョン、たとえば4.19.0というバージョン名の付け方はセマンティックバージョニングというルールに従っている。
↓のように、それぞれの数字の意味が メジャーバージョン.マイナーバージョン.パッチバージョンの意味をもつ。
ちなみに、後方互換とはAPIのバージョンが上がっても、旧APIと互換性が保たれるという意味。
さきほどにもどると、キャレット表記つまり^4.19.0が4.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 ・ ・ ・
というわけでpackage-lock.jsonは自動生成されるファイルだが(npm5以降自動生成されるようになった)、gitにcommitすることが推奨されている。
ダウンロードしてきた依存パッケージはどこに保存される?
さて、いま依存パッケージがたくさんあることをみてきたが、それらパッケージはnpm installされたあと、どこに保存されるのか。現在のディレクトリ構成↓を再度みてみると、node_modulesというディレクトリも自動的に生成されている。
現在のディレクトリ構成
es6hello ├── node_modules ├── package.json └── package-lock.json
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>
<script src="main.js"></script>
次に、jsのソースコードを格納するためのsrcディレクトリを作り、そこにindex.jsという名前のファイルを作る。
index.js
alert("hello");
ディレクトリ構成は↓のようになる
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
> 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
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>
package.json(抜粋)
"scripts": { "start": "webpack-dev-server",
仮に以下のような定義をしていた場合には
package.json(抜粋)
"scripts": { "dev": "webpack-dev-server",
npm run dev
よく使うコマンドは特別扱いされていて、"start"、stop、restart、testなどは、
npm start
つまり npm run-script start は npm 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; }
exportとは何か?
exportとは、このモジュールにあるクラスや変数を外部のファイルからも使えるよ、という宣言のこと。外部のファイル側からは import文を使うことで利用できるようになる。
export defalut とは何か
export defalutとは何か。export default class Hello
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(); greeting.sayHello();
import Greeting from './hello.js';
import以降このクラスは Greeting という名前で使うことができるので、
このクラスを new するには以下のようになる。
const greeting = new Greeting();
起動してみる
npm start
無事実行できている。
(だが、実際にはJavascriptをES6の記法のまま実行しているのでブラウザによっては動作しない場合がある、その為の対策=babelは後で説明する)
ちなみに、コンソールのほうにも出力されている。
★ここまで(「クラスを定義する」)の全ソースコードはこちら★
現在のディレクトリ構成は以下のとおり。
es6hello ├── node_modules ├── src │ ├── hello.js │ └── index.js ├── .gitignore ├── index.html ├── package.json └── package-lock.json
<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");
下にでてくる
contentBase: path.join(__dirname, ''),
mode: 'development',
モードは、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",
openPage: "index.html"は、自動的にブラウザを起動するときに開くページを指定している。
contentBase: path.join(__dirname, "public"),
ここで指定している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,
これにより index.html などを編集して保存すると即座にブラウザがリロードされる。
ちなみに、jsのコードのほうはデフォルトでオートリフレッシュ機能が有効となっているので、これでjsファイルもhtmlも編集したらブラウザのリロードが自動で動作するようになった。
コマンドプロンプト
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>
<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';
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' }
umdについては次の章で触れる。
実行
ここまでで実行するとnpm start
★ここまで(「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' } };
webpack.config.jsの変更点
entry: {app: './src/index.js'}, output: { publicPath: "/js/", filename: '[name].js', library: ["com", "example"], libraryTarget: 'umd' }
エントリーをセットする
entry: {app: './src/index.js'},
複数のエントリーを扱いときのため、↑のように、連想配列にして指定できる。この例では「appというキーに対して./src/index.jsがエントリーとしてセットされますよ」という意味となる。このペアを複数セットしておくと、複数のバンドルを生成することができる。今は複数不要だが、複数になってもいいようにこの記法で書いておく。
バンドルjsのファイル名をセットする
filename: '[name].js',
[name]というのはメタ記法で、[name]の中に、上のエントリーで書いたkey名が入るようになる。
上では
{app: './src/index.js'}
としているので、キーがappとなるので、バンドル生成時には app.js というファイルが生成される。webpack-dev-serverからアクセスする場合も同じ。publicPathをセットする
publicPath: "/js/",
この例だと、ブラウザには
<script src="js/app.js"></script>
と記述する。つまりjs/というパスでアクセスできるようになるということ。パッケージ名をセットする
library: ["com", "example"],
**com.example*のようにピリオドでつないだパッケージ名にすることができる。
パッケージ名は、以下のような形式でexportしたクラスをnewするときに使える。
const greeting = new com.example.Greeting();
libraryTargetでライブラリ化の方式を指定
libraryTarget: 'umd'
ほかにも 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>
<script> const greeting = new com.example.Greeting(); alert(greeting.sayHello()); </script>
com.example
というパッケージ名でGreeting
クラスにアクセスしている部分となる。
実行する
npm start
babel(バベル)を使ってすべてのブラウザで動作するようにする
ここまでいくらかJSコードを書いてきたが、これらのコードはすべて ES6 (ECMAScript 6)の記法で書いてきた。ES6をサポートしていない古いブラウザも未だあるため、こうした古いjsランタイムを搭載したブラウザ向けのjsコードは古いjsコードつまりES5 (ECMAScript 5)のほうが無難である。
でも 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
{ "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" } }
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'
options
以下にはbabel側に伝える挙動を記載する。
@babel/preset-envの意味
(webpack.config.jsから抜粋)
presets: [ [ "@babel/preset-env", { "useBuiltIns": "usage", "targets": "> 0.25%, not dead" } ] ]
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"] }
https://babeljs.io/docs/en/next/babel-preset-env.html
ソースマップを出力する
さて、そろそろ終盤に入ってきた。webpack.config.jsに追加した以下の部分は、ソースマップの出力を有効にしている。
devtool: 'inline-source-map'
今回はソースはES6記法で書くが、実際にブラウザ上で実行されるコードはbabelによって変換されたコードとなるため、もしエラーが発生してもこのままだと変換後のコードの行番号でスタックトレースが表示されてしまう。
ここで、上記のソースマップの設定をしておくと、ブラウザで実行するときにも、ソースコードの情報がきちんと埋め込まれる。
npm start
ここまでで 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"
package.json
"scripts": { "start": "webpack-dev-server", "build": "webpack --config webpack.config.js", "test": "echo \"Error: no test specified\" && exit 1" },
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]
ここまでのディレクトリ構成
es6hello ├── dist ├── node_modules ├── public │ └── index.html ├── src │ ├── hello.js │ └── index.js ├── .gitignore ├── package.json ├── package-lock.json └── webpack.config.js
ライブラリを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
{ "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する
作ってきたファイルを 最初に作った githubにpushする。git push
npmjs.comにサインアップする
https://www.npmjs.com/にて、画面に従いサインアップする(サインアップがまだの場合)
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
https://www.npmjs.com/package/es6hello
まとめ
今風のjs開発の知識ゼロ状態から、node.jsの環境、npm、webpack4、babelなど使いハンズオンでnpmモジュールを作るところまで説明してきました。このプロジェクトは https://github.com/riversun/es6hello に公開しています。
jsフロントエンドプロジェクトのひな形としても活用可能です。
コメント
コメントを投稿