JSの分割ファイル読み込み(require vs import)メモ
JSの分割ファイル読み込み(require vs import)メモ
2019年3月時点でのNode.js・ブラウザ環境での分割ファイル読み込み(require, import/export)についてのメモ.
結論:
開発側ではESM(
import
)を利用してコードを書いて管理.ただし,全ブラウザでESMは使えない.
babelでNode.jsが扱える
require
に統一.webpackなどのバンドラで依存関係を解決して1つのjsファイルにまとめる.
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 -> 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を利用すると,
import
はrequire
関数に変換される.
以上から,babelを使えばrequire
に変換してくれるので混在を意識しなくてもrequire
に統一してくれる.
ただし,babelは依存関係を解決せずrequireに変換するのみなのでこのままではブラウザでは動作させず,依存関係を解決してくれるバンドラ(ex.browserify, webpack)が必要になる.
実行環境まとめ
\ | require |
import/export |
---|---|---|
Node.js | ○ | ✖ (babelでrequireに変換可) |
Client(browser) | ✖ | ○ |
Node.js --experimental-module | ✖ | ○ |
バンドル化
require
を解決できる環境をつくる.
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
> Hash: ee50f27dc6c54b4c11c8 > Version: webpack 4.29.6 > Time: 682ms > 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をブラウザで開いてコンソールを見ると,実行されているのがわかる.
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
で直接実行していく
」ということをやってメインファイルの依存を解決しているようだ.