67
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JSのモジュールとbabelとwebpackとは何かまとめてみる(初心者向け)

Last updated at Posted at 2019-11-25

初心者向けにモダンJavaScriptの開発で必要になる
CommonJSとECMAScriptでのモジュールの違い、babelとwebpackの知識に関してまとめておきます。

CommonJSとECMAScript

JavaScriptの歴史的経緯にもなるのですが、
実はJavaScriptには
サーバサイドのNodeJS(CommonJS)とブラウザのJavaScript(ECMAScript)
の大まかに2つの言語仕様があります。1

元々のJavaScriptの欠点としてJavaScript内でモジュール化されたJavaScriptファイルを外部参照するという言語機能がなかったことが起因しています。
(かつてはHTMLのscriptタグでグローバル参照するというやり方が従来だった)
モジュール機能が先に言語仕様として策定されたのがCommonJSでECMAScriptのモジュール機能は後発となります。
したがって、大まかな文法はほぼ一緒なもののモジュール機能の記述に関して、CommonJSとECMAScript間で大きな違いがあります。
なお、ECMAScriptには仕様策定を決めているTC39という委員会があります。

参考:JavaScriptが辿った変遷

CommonJSとECMAScriptのModulesの違い

細かい違いは以下の記事のほうが参考になります

参考:CommonJS と ES6の import/export で迷うなら

CommonJSのモジュール機能

CommonJSでモジュールを外部参照できるようにするためには、主にmodule.exportsを使います。
ここだとabc.jsというファイル名のモジュールを作成します。

abc.js
module.exports = 変数

CommonJSでモジュールを参照するためには、requireを使います。
requireには参照するモジュールのファイル名を指定します。
abcにはmodule.exportsした変数そのものが参照できます。

const abc = require('abc')

ECMAScriptのモジュール機能

ECMAScriptでモジュールを外部参照できるようにするためには、主にexportもしくはexport defaultを使います。(exportsでないのに注意)
ここだとdef.jsというファイル名のモジュールを作成します。

abc.js
export 変数
export default 変数

ECMAScriptでモジュールを参照するためには、importを使います。
importには参照するモジュールのファイル名を指定します。

import { 変数 } from 'def' // exports 変数を参照する場合
import def from 'def' // exports default 変数を参照する場合
// import { default as def } from 'def' // こうもかける

ちなみにimport文はトップレベルでないと使えないという制限があります。

// トップレベル
import def from 'def' // OK

if (条件分岐) {
  // ブロック{}で囲まれている箇所はトップレベルでない
  import def from 'def' // NG
}

dynamic import

Promise形式で外部モジュールを非同期に参照読み込みすることもできます。(dynamic import)
これにより、うまく使いこなせれば読み込みのオーバヘッドをなくすことができます。(並列読み込みによる高速化)
また、こちらに関してはトップレベルでなくても使えます。

import('def').then(module => { /* 処理 */ })

参考:Chrome、Safari、Firefoxで使えるJavaScriptのdynamic import(動的読み込み)

NodeJSからECMAScript Module(ESM)のimport、exportができるようになる(将来的に)

現在、NodeJS側からESMを参照できるようにするという流れでNodeJSチームが頑張って対応しています。2
Plan for New Modules Implementation

NodeJS 13.2.0から--experimental-modulesフラグ無しでESMのimport、exportが使える

ついにNode 13.2.0(2019/11/21リリース)から--experimental-modules無しでESMのimport、exportが使えるようになりました。(Phase4)
拡張子も.jsのままで.mjsにする必要はなく、package.jsonに"type":"module"の設定を追加するだけでESMのimport、exportが使えます。
公式:ECMAScript Modules

package.json
{
  "type": "module",
  "name": "node13.2",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "body-parser": "^1.19.0",
    "express": "^4.17.1"
  }
}

ESMのimportを使ってNodeJSのサーバを立ててみます。

app.js
import { default as express } from 'express'
import { default as bodyParser } from 'body-parser'
const app = express()

const wrap = (fn) => (req, res, next) => fn(req, res, next).catch(err => {
  console.error(err)
  if (!res.headersSent) {
    res.status(500).json({message: 'Internal Server Error'})
  }
})
process.on('uncaughtException', (err) => console.error(err))
process.on('unhandledRejection', (err) => console.error(err))
process.on('SIGINT', () => process.exit(1))

app.use(express.static('dist'))
app.use(bodyParser.urlencoded({extended: true}))
app.use(bodyParser.json())

app.get('/api', wrap(async (req, res) => {
  res.json('hello world!!')
}))

app.listen(3000, () => {
  console.log('Access to http://localhost:3000')
})

警告は出ますが、babel無しでESMのモジュールのimportができるようになりました。

$ node app.js 
(node:62642) ExperimentalWarning: The ESM module loader is experimental.
Access to http://localhost:3000

babel

フロントエンドの実装にはブラウザ間でHTML、CSS、JSの実装レベルが異なるという大きな問題があります。
ブラウザの各機能の実装状況はCan I useなどで確認できます。
この問題に関して新しいJS文法を古いJS文法に変換(トランスパイル)して古いブラウザでも使えるようにするためのツールがbabelです。
babelの役割は非常に多く、多数のプラグインとpresetが存在しています。
例えば以下のようなことが実現できます。

Polyfill参考:Babel 7.4.0で非推奨になった@babel/polyfillを使わず、core-js@3で環境構築する
TypeScript参考:Babel 7でTypeScriptをトランスパイルしつつ型チェックをする 〜webpack 4 + Babel 7 + TypeScript + TypeScript EsLint + Prettierの開発環境を構築する〜

babelの設定ファイル

.babelrc, babel.config.js, babel.config.cjs, babel.config.jsonのいずれかのファイルに設定を書くか
webpackの場合、後述のbabel-loaderプラグインのoptionsに指定する方法があります。
(babel実行時に自動に設定ファイルを探しに行く)
主にpluginと複数のpluginをまとめたpresetを指定することがほとんどです。

公式:Configure Babel

例えば.babelrc、babel.config.jsonの場合は次のように書きます。

{ 
  "plugins": ["@babel/plugin-proposal-optional-chaining"],
  "presets": [
    ["@babel/preset-env", {
       "useBuiltIns": "usage",
       "corejs": 3,
       "modules": false
    }]
  ] 
}

babel.config.js、babel.config.cjsの場合は次のように書きます。

module.exports = function (api) {
  api.cache(true) // この変換設定関数をキャッシュする

  const presets = [
   ["@babel/preset-env", {
      "useBuiltIns": "usage",
      "corejs": 3,
      "modules": false
   }]
  ]
  const plugins = ["@babel/plugin-proposal-optional-chaining"]

  return {
    presets,
    plugins
  }
}

babelの実行

babelのトランスパイルには@babel/core、@babel/cliが必要です。

# package.json作成
$ npm init -y
# babelのコマンドラインツールのインストール
$ npm install @babel/core @babel/cli 
# babelのプラグインのインストール
$ npm install @babel/plugin-proposal-optional-chaining
# polyfill系
$ npm install @babel/preset-env core-js regenerator-runtime

例えば、次のようなindex.jsを作成します。

index.js
const message = {hello: 'Hello', world: 'world!'}

console.log(message?.hello + '' + message?.world)

.babelrcもしくはbabel.config.jsを作成し、次のコマンドを実行します。

$ babel index.js --out-file out.js

変換後のout.jsは次のようになります。
(ES5文法への変換、optional-chaining文法が変換される)

out.js
var message = {
  hello: 'Hello',
  world: 'world!'
};
console.log((message === null || message === void 0 ? void 0 : message.hello) + '' + (message === null || message === void 0 ? void 0 : message.world));

webpack

webpackは、モダンJavaScriptアプリケーション用の静的モジュールバンドルです。
webpackがアプリケーションを処理するとき、プロジェクトが必要とするすべてのモジュールをマップし、1つ以上のバンドルを生成する依存関係グラフを内部で構築します。
要はwebpackでビルドすることでnode_modules以下の依存関係も含め、1つのJSファイルにまとめることができます。
コアなコンセプトとして以下の機能があります。

  • Entry:ビルド対象のアプリケーションエントリーポイントとなるファイル
  • Output:出力先のフォルダ、ファイル名
  • Loaders:ビルド用ローダー(メインの変換処理を行うための設定)
  • Plugins:ビルドの補助プラグイン
  • Mode:development/productionビルドかの指定
  • Browser Compatibility:ブラウザ最適化

公式:Concept

バージョン4.0.0以降、webpackはプロジェクトをバンドルするための設定ファイルを必ずしも必要としません。
ただし、デフォルトの設定を上書きする際はwebpack.config.jsを作成して設定を記述します。

Entry

エントリポイントは、webpackが内部依​​存関係グラフの構築を開始するために使用するモジュールを示します。
webpackは、エントリポイントが依存する他のモジュールとライブラリを(直接的および間接的に)把握します。
デフォルトでは、その値は./src/index.jsですが、webpack構成でentryプロパティを設定することにより、異なる(または複数のエントリポイント)を指定できます。

webpack.config.js
module.exports = {
  entry: './path/to/my/entry/file.js'
};

Output

outputプロパティは、作成したバンドルを出力する場所とこれらのファイルに名前を付ける方法をwebpackに指示します。
デフォルトでは、メイン出力ファイルの場合は./dist/main.js、その他の生成されたファイルの場合は./distフォルダになります。

設定ファイルにて出力先を変更することも可能です。

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

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),  // 出力するフォルダ名
    filename: 'my-first-webpack.bundle.js' // 出力するメインファイル名
  }
};

Loader

すぐに使えるwebpackは、JavaScriptファイルとJSONファイルのみを理解します。Loaderを使用すると、webpackは他の種類のファイルを処理し、アプリケーションで使用して依存関係グラフに追加できる有効なモジュールに変換できます。

大まかに言うと、LoaderにはWebpack構成に2つのプロパティがあります。

  1. testプロパティは、変換する対象のファイルを識別します。
  2. useプロパティは、変換を行うために使用するLoaderを指定します。
webpack.config.js
const path = require('path');

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js'
  },
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  }
};

Plugins

Loaderは特定のタイプのモジュールを変換するために使用されますが、
プラグインを活用して、バンドルの最適化、リソース管理、環境変数の注入などの幅広いタスクを実行できます。
プラグインを使用するには、require()してplugins配列に追加する必要があります。
ほとんどのプラグインはオプションでカスタマイズできます。
プラグインはさまざまな目的のためにconfigで複数回使用できるため、new演算子で呼び出してプラグインのインスタンスを作成する必要があります。

webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm
const webpack = require('webpack'); //to access built-in plugins

module.exports = {
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};

Mode

modeパラメーターをdevelopment、production、またはnoneに設定することにより、
各環境に対応するwebpackの組み込みの最適化を有効にできます。
デフォルト値はproductionです。

webpack.config.js
module.exports = {
  mode: 'production'
};

Browser Compatibility

webpackは、ES5準拠のすべてのブラウザーをサポートしています(IE8以下はサポートされていません)。
webpackでは、import()およびrequire.ensure()にPromiseが必要です。
古いブラウザをサポートする場合は、
これらの文法を使用する前にpolyfillをロードする必要があります。

webpackの実行

次のような簡単なReactのプロジェクト構成でビルドしてみます。

├── dist
├── index.html
├── package.json
├── src
│   └── index.tsx
├── tsconfig.json
└── webpack.config.js

webpackのビルドにはwebpackwebpack-clibabel-loaderのパッケージが追加で必要です。
今回はReactのプロジェクトのため、reactreact-dom@babel/preset-reactを追加します。
さらにtypescriptを使うため、typescript@babel/preset-typescript@types/react@types/react-domのパッケージが必要です。
@babel/preset-typescriptは型チェックをしてくれないため、tscで並列で型チェックを行います。
npm-run-allを使うとrun-pコマンドで並列でコマンドを実行することができます。
webpackコマンドでwebpack.config.jsの設定でプロジェクトをビルドすることができます。
package.jsonは次のようになります。

package.json
{
  "name": "test",
  "version": "1.0.0",
  "license": "MIT",
  "scripts": {
    "start": "run-p check-types webpack",
    "check-types": "tsc -w",
    "webpack": "webpack --watch"
  },
  "devDependencies": {
    "@babel/cli": "^7.7.4",
    "@babel/core": "^7.7.4",
    "@babel/plugin-proposal-optional-chaining": "^7.7.4",
    "@babel/preset-env": "^7.7.4",
    "@babel/preset-react": "^7.7.4",
    "@babel/preset-typescript": "^7.7.4",
    "@types/react": "^16.9.13",
    "@types/react-dom": "^16.9.4",
    "babel-loader": "^8.0.6",
    "core-js": "^3.4.2",
    "html-webpack-plugin": "^3.2.0",
    "npm-run-all": "^4.1.5",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "regenerator-runtime": "^0.13.3",
    "typescript": "^3.7.2",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  }
}

index.tsxにReactの最小のプログラムを記載します。
Reactの型に関しては次のサイトが参考になります。

参考:TypeScript Deep Dive 日本語版

index.tsx
import React from 'react'
import ReactDOM from 'react-dom'

const message = {hello: 'Hello', world: 'world!'}

const App: React.SFC<{message: string}> = (props) => {
  return <h1>{props.message}</h1>
}

ReactDOM.render(
  <App message={`${message?.hello} ${message?.world}`} />,
  document.getElementById('root')
)

vscodeの場合、Optinal Chainingのエラー表記が出る場合は以下の設定を行うと解消されます
TypeScriptのOptional Chainingは用法用量を守って正しく使え

typescriptのビルド設定をtsconfig.jsonに記載します。
今回はtypescriptのビルドには@babel/preset-typescriptを使うため、型のチェックのみを行います。
"noEmit": trueが重要)

tsconfig.json
{
  "compilerOptions": {
    /* トランスパイル後のECMAScriptのバージョン */
    "target": "ES2019",

    /* 相対パスではないモジュールは node_modules 配下を検索する */
    "moduleResolution": "node",

    /* 今回、トランスパイルは Babelが行うので、`tsc`コマンドでJavaScriptファイルを出力しないようにする */
    "noEmit": true,

    /* 厳格な型チェックオプション(noImplicitAny、noImplicitThis、alwaysStrict、
       strictBindCallApply、strictNullChecks、strictFunctionTypes、
       strictPropertyInitialization)を有効化する */
    "strict": true,

    /* 各ファイルを個々のモジュールとしてトランスパイルする。
       Babel では技術的制約で、ネームスペースなどのファイルを跨いだ構文を解釈してトランスパイルできない。
       このオプションを有効にすれば、Babel でトランスパイルできない TypeScriptの構文を検出して警告を出す */
    "isolatedModules": true,

    /* ES modules 形式以外の、CommonJS 形式などのモジュールを default import 形式で読み込める
       例)const module = require('module') -> import module from 'module' */
    "esModuleInterop": true,

   /* Reactの場合、JSX文法のチェックを有効にする */
    "jsx": "react"
  },
  "include": ["src/**/*"]
}

webpack.config.jsにwebpackのビルド設定を記述します。

webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

module.exports = {
  mode: 'development', // 開発モードビルド
  entry: './src/index.tsx', // ビルド対象のアプリケーションのエントリーファイル
  devtool: "source-map", // ソースマップを出力するための設定、ソースマップファイル(.map)が存在する場合、ビルド前のソースファイルでデバッグができる
  output: {
    path: path.resolve(__dirname, 'dist'),  // 出力するフォルダ名(dist)
    filename: 'bundle.js' // 出力するメインファイル名
  },
  module: {
    rules: [
      { 
        test: /\.ts(x?)$/, // .ts .tsxがbabelのビルド対象
        exclude: /node_modules/, // 関係のないnode_modulesはビルドに含めない
        use: {
          loader: 'babel-loader', // babel
          options: {
            presets: [
              // polyfillのpreset
              ['@babel/preset-env', {
                useBuiltIns: 'usage',
                corejs: 3,
                modules: false // ECMAScript向けビルド
              }],
              // reactのpreset
              '@babel/preset-react',
              // typescript→javascript変換のpreset
              '@babel/preset-typescript'
            ],
            // optional-chaining文法を使うためのプラグイン
            plugins: ['@babel/plugin-proposal-optional-chaining']
          },
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({template: './index.html'}) // ビルドしたbundle.jsをindex.htmlに埋め込む
  ]
}

index.htmlは以下のようになります。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>タイトル</title>
</head>
<body>
  <div id='root'></div>
<!-- webpackビルドが完了するとHtmlWebpackPluginによりビルド済みのbundle.jsが埋め込まれる -->
<!-- <script type="text/javascript" src="bundle.js"></script></body> -->
</body>
</html>

ビルドには次のコマンドを実行します。
成功するとdistフォルダにbundle.jsが埋め込まれたindex.htmlが生成されます。

$ npm start

webpack is watching the files…

Hash: e532e63e7e9060154c4e
Version: webpack 4.41.2
Time: 1350ms
Built at: 2019-11-25 17:56:00
        Asset       Size  Chunks                   Chunk Names
    bundle.js    1.1 MiB    main  [emitted]        main
bundle.js.map   1.26 MiB    main  [emitted] [dev]  main
   index.html  326 bytes          [emitted]        
Entrypoint main = bundle.js bundle.js.map
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {main} [built]
[./src/index.tsx] 495 bytes {main} [built]
    + 65 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
    [./node_modules/html-webpack-plugin/lib/loader.js!./index.html] 484 bytes {0} [built]
    [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {0} [built]
    [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 1 hidden module
[17:56:01] Found 0 errors. Watching for file changes.


webpackのミドルウェア

webpackのミドルウェアを導入することができれば、さらに開発の効率があがります。
導入方法だけでそれぞれ記事ができてしまうため、ここでは紹介までにしておきます。

  • webpack-dev-derver:開発用のサーバを起動して、その上でwebpackを実行するためのミドルウェア。HMR(Hot Module Replacement)なども設定でき、プログラム修正時にブラウザをリロードすることなくモジュールの再読み込みを行ってくれる。
  • webpack-dev-middleware:バックエンド(NodeJS)でwebpackビルドができるようになるミドルウェア
  • webpack-hot-middleware:バックエンドのwebpackビルド時にHMRしてくれるミドルウェア

Parcel

webpackに関してはビルドの設定の自由度が高い反面、ビルドの設定が難しいという問題があります。
簡易的なプロジェクトであれば、静的バンドラーの一種であるParcelを使うという選択肢もあります。
Parcelはビルド設定ファイルがなく裏側で色々よしなにやってくれる反面、細かいビルド設定ができないという欠点もあります。
本格的なプロジェクトの場合はwebpackをマスターしたほうが後々良いでしょう。

参考:(兎に角)早くプロトタイプを作る技術(初心者向け)

  1. 正確には亜種なスクリプト仕様はいくつかあるのですが一旦置いときます(Google Apps ScriptとかJScriptとか・・・)

  2. ESMがNodeJS側で参照できるようになれば、npmであらゆるパッケージがバックエンド、フロントエンドの垣根を超えて使えるようになる未来がくるのかも・・・?(さらにエコシステム化が進む)

67
51
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
67
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?