はしくれエンジニアもどきのメモ

情報系技術・哲学・デザインなどの勉強メモ・備忘録です。

JSの分割ファイル読み込み(require vs import)メモ

JSの分割ファイル読み込み(require vs import)メモ

2019年3月時点でのNode.js・ブラウザ環境での分割ファイル読み込み(require, import/export)についてのメモ.

結論:

  1. 開発側ではESM(import)を利用してコードを書いて管理.

  2. ただし,全ブラウザでESMは使えない.

  3. babelでNode.jsが扱えるrequireに統一.

  4. webpackなどのバンドラで依存関係を解決して1つのjsファイルにまとめる.

  5. htmlはそのjsファイルをscriptタグで読み込む.

環境

  • Windows10
  • node v10.14.2
    • npm 6.9.0

JSでの分割ファイル読み込み(書き方)

基本的にはrequireとimportの2種類がある. 似た振る舞いをするが,元の仕様が違うので片方の環境でしか動作しない.

どちらも外部読み込みしたい変数・関数にはexportの設定が必要になる.

require

requireは,Node.js(CommonJS系)で動作し, ブラウザ(クライアントJS)では動作しない.

呼び出し先

// 拡張子は省略できる, 相対パスもok
const Lib = require('Lib_req.js');
var v = Lib.variable;
var f_v = Lib.func();

呼び出し元

外部呼出ししたい変数,関数をmodule.exportsオブジェクトに設定する.

Lib_req.js:

// not es6
var variable = "req";
function _f(){
  return "_f()";
}
function func(){
  return _f() + "f()";
}

module.exports = {
  variable: variable,
  func: func,
  // exportsに設定しない限り_f()は呼べない
}

import/export

import/exportは,別名ESModule(ESM),EcmaScriptの仕様である.つまり,ブラウザ(クライアントJS)で動作でする.

呼び出し先

importの場合,読み込みたいオブジェクトのみの指定もできる.

対応ブラウザであれば <script type="module" src="Lib_im.js"></script> のようにtype="module"で呼び出しできる.

// 拡張子は省略できる, 相対パスもok
import * as Lib from 'Lib_im.js';
var v = Lib.variable;
var f_v = Lib.func();

呼び出し元

外部呼出ししたい変数,関数にexport文を設定する.これがないオブジェクトは呼び出しできない.

Lib_im.js:

// not es6
export var variable = "im";
function _f(){
  return "_f()";
}
export function func(){
  return _f() + "f()";
}

require, import/exportの実行方法

requireありコードの実行

requireを解釈できるのは,Node.JS(CommonJS)である. nodeコマンドで実行できる.

node index.js

importありコードの実行

ESM(import/export, type="module")に対応しているのは,クライアントJS(EcmaScript)である. ただし,IE11以外の最新版のモダンブラウザの場合のみである.

特にdynamic import(import文をPromiseのように使える)はまだ対応ブラウザが少ない.

また,クライアントサイドでは最新のES6やES7のコードが対応してないことが多いため,Babelなど利用したpolyfillが必要になってくる.ただ,importの直接のpolyfillが存在せず,babelではrequire関数に変換されるのみである.

babel index.es --out-file index.js // import -&gt; require()

babelはトランスパイルのみで読み込み順など依存関係の解決はしてくれない.この場合,browserifyやwebpackといったバンドラが必要になる.

Node.jsでESMを動かす

Node.jsでも実験オプション(--exprimental-module)を使うことでESMのコードを動かせる.

ただし,いくつか制約がある.

  • ファイル拡張子は.mjsでないといけない.
  • ただし,CommonJSからESの仕様で動作するのでrequireなどが使えなくなる.
node --experimental-module index.mjs

つまり,require, importの混在したコードは--exprimental-moduleオプションありでは実行できない.

require, import混在コード問題

  • Node.jsではrequireが使える.

  • ESM(import/export)は,対応ブラウザのみで動作可能,それ以外での動作を考えると依存関係(順番)を考慮したscriptタグを埋め込むくらいしか解決策がない. また,babelを利用すると,importrequire関数に変換される.

以上から,babelを使えばrequireに変換してくれるので混在を意識しなくてもrequireに統一してくれる.

ただし,babelは依存関係を解決せずrequireに変換するのみなのでこのままではブラウザでは動作させず,依存関係を解決してくれるバンドラ(ex.browserify, webpack)が必要になる.

実行環境まとめ

\ require import/export
Node.js ✖ (babelでrequireに変換可)
Client(browser)
Node.js --experimental-module

バンドル化

requireを解決できる環境をつくる.

バンドルのイメージ(webpackのトップページ)

webpackの導入:

npm install --save-dev webpack webpack-cli

babelの導入:

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

babel-loaderがあると, バンドルする前にentry指定したjsをbabelに通してからバンドルしてくれる. (また,babel-loaderは,webpackがない状態でインストールしようとすると警告がでる.)

プラグインの導入:

npm install --save-dev html-webpack-plugin

html-webpack-pluginは,バンドルしたjsファイルを,指定したhtmlファイルに自動でscriptタグとして挿入してくれる.

想定のファイル構成:

projectdir/
  src/
    testRequire.es
    testTarget.es
    template.html // (html-webpack-pluginでの元)
  public/
    bundle.js // webpackで生成
  index.html // (html-webpack-pluginで生成)
  package.json
  webpack.config.js

ソースコード

src\testRequire.es:

requireができるファイル

const hoge = "hoge";

function hello(){
  return "hello";
}

function fuga(){
  return "fuga";
}

module.exports = {
  hoge,
  hello,
  fuga,
};

src\testTarget.es:

importができるファイル

export const hoge = "hoge";

export function hello(){
  return "hello";
}

function sum(x, y, z) {
  return x + y + z;
}

export function fuga(){
  let arr = [1, 2, 3];
  return sum(...arr);
}

index.es(メインのjs):

import * as testTarget from './src/testTarget.js';
const testReq = require('./src/testRequire.js');

console.log(testTarget.hoge);
var s = testTarget.hello();
console.log(s);
var f = testTarget.fuga();
console.log(f);


console.log(testReq.hoge);
console.log(testReq.hello());
console.log(testReq.fuga());

webpack.config.js(webpackの設定ファイル):

// output.pathに絶対パスを指定する必要があるため、pathモジュールを読み込んでおく
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // モードの設定、v4系以降はmodeを指定しないと、webpack実行時に警告が出る
  mode: 'development',
  // エントリーポイントの設定
  entry: './index.es',
  // 出力の設定
  output: {
    // publicPath: '/',
    // 出力するファイル名
    filename: 'bundle.js',
    // 出力先のパス(絶対パスを指定する必要がある)
    path: path.join(__dirname, 'public/js')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/template.html',
      filename: path.join(__dirname, 'index.html'),
    }),
  ],
  // ローダーの設定
  module: {
    rules: [
      {
        // ローダーの処理対象ファイル
        test: /\.es$/,
        // ローダーの処理対象から外すディレクトリ
        exclude: /node_modules/,
        use: [
          {
            // 利用するローダー
            loader: 'babel-loader',
            // ローダーのオプション
            // 今回はbabel-loaderを利用しているため
            // babelのオプションを指定しているという認識で問題ない
            options: {
              presets: ['@babel/preset-env'],
            }
          }
        ]
      }
    ]
  },
};

実行:

いろいろオプションも指定できるが,webpackコマンドだけで生成できる.

npx webpack
&gt; Hash: ee50f27dc6c54b4c11c8
&gt; Version: webpack 4.29.6
&gt; Time: 682ms
&gt; Built at: 2019-03-26 17:05:22
           Asset       Size  Chunks             Chunk Names
..\..\index.html  146 bytes          [emitted]
       bundle.js   5.77 KiB    main  [emitted]  main
Entrypoint main = bundle.js
[./index.es] 302 bytes {main} [built]
[./src/testRequire.js] 217 bytes {main} [built]
[./src/testTarget.js] 389 bytes {main} [built]
Child html-webpack-plugin for "..\..\index.html":
     1 asset
    Entrypoint undefined = ..\..\index.html
    [./node_modules/html-webpack-plugin/lib/loader.js!./src/template.html] 286 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

これでbundle.jsが生成される.

できあがったindex.htmlをブラウザで開いてコンソールを見ると,実行されているのがわかる.

htmlを開く

webpackの「依存関係の解決」は何をやっているか

よく依存関係の解決といわれるが具体的に何をしているのかbundle.jsの中を見る.

まず,require関数の代わりに__webpack_require__関数が定義される.

   // The require function
/******/    function __webpack_require__(moduleId) {
/******/
/******/        // Check if module is in cache
/******/        if(installedModules[moduleId]) {
/******/            return installedModules[moduleId].exports;
/******/        }

....(省略)

ソースコードファイルのパスとそのソースコードをkey/valueとして使って,次のようなオブジェクトを作る.

{
  "./index.es": eval(< index.esのソースコード >),
  "./src/testRequire.es": eval(< ./src/testRequire.esのソースコード >),
  "./src/testTarget.es": eval(< ./src/testTarget.esのソースコード >)
}

また,オリジナルソースコードrequire部分は__webpack_require__に置き換えられている.

/******/ ({

/***/ "./index.es":
/*!******************!*\
  !*** ./index.es ***!
  \******************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _src_testTarget_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./src/testTarget.js */ \"./src/testTarget.js\");\n/* harmony import */ var _src_testTarget_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_src_testTarget_js__WEBPACK_IMPORTED_MODULE_0__);\n\n\nvar testReq = __webpack_require__(/*! ./src/testRequire.js */ \"./src/testRequire.js\");\n\nconsole.log(_src_testTarget_js__WEBPACK_IMPORTED_MODULE_0__[\"hoge\"]);\nvar s = _src_testTarget_js__WEBPACK_IMPORTED_MODULE_0__[\"hello\"]();\nconsole.log(s);\nvar f = _src_testTarget_js__WEBPACK_IMPORTED_MODULE_0__[\"fuga\"]();\nconsole.log(f);\nconsole.log(testReq.hoge);\nconsole.log(testReq.hello());\nconsole.log(testReq.fuga());\n\n//# sourceURL=webpack:///./index.es?");

/***/ }),

/***/ "./src/testRequire.es":
/*!****************************!*\
  !*** ./src/testRequire.es ***!
  \****************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

....省略

「 entrypointのメインファイルを実行していき,requireが来るたびに__webpack_require__関数でパスをkeyとしてオブジェクトから該当ソースを参照しevalで直接実行していく 」ということをやってメインファイルの依存を解決しているようだ.