Node.jsでimport・export(ES6の構文)を使えるようにwebpack × Babelの設定をやってみた
はじめに
こんにちは、SHIFT の開発部門に所属しているKatayamaです。今期から転属になり、開発を担当していくことになりました。
今回はNode.jsに関しての記事を書いていこうと思います。Node.js の公式サイト(v14.8.x)を見てわかるように、以下の通り require/module.export を使った実装になっていると思います。
const http = require("http");
const hostname = "127.0.0.1";
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain");
res.end("Hello, World!\n");
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
require(上記にはないが module.export) はモジュール管理に関する実装ですが、Node.js ではこの書き方でないとコードの実行できません。
そのためNode.jsで import/export (ES Modules)などES6の構文で実装したい場合には、
・Determining module systemに書かれている設定をする
・Babelでコードの変換をする
といった追加の設定が必要になります。今回はフロントエンドでも使われているBabelでコードの変換する、という方法でNode.jsでも import/export を使えるようにしてみたので、それについて見ていきたいと思います。
Node.js で import/export を使う
どうすればいいか?の結論から、どうしてそうすれば Node.js で import/export を使った実装ができるのか?の理由・詳細についてみていく。
結論:どうすればいいか?
tree -I node_modules
.
├── dist
│ └── main.js
├── .babelrc
├── package.json
├── src
│ └── index.js
├── webpack.config.js
└── yarn.lock
// webpack.config.js
module.exports = {
target: "node",
entry: "./src/index.js",
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
],
},
};
// .babelrc
{
"presets": ["@babel/preset-env"]
}
webpack で build するコードには以下を追記(npm install または yarn add も)。
import "core-js/stable";
import "regenerator-runtime/runtime";
yarn add --dev core-js
yarn add --dev regenerator-runtime
webpack コマンドを実行(build)するコードの全体としては以下のような感じになる。
// index.js
import "core-js/stable";
import "regenerator-runtime/runtime";
import express from "express";
const app = express();
app.use(express.json());
app.get("/", async (req, res) => {
const reqTime = Date.now();
console.log(Array.from("foo"));
await new Promise((resolve) => {
setTimeout(() => {
resolve("sleep");
}, 500);
});
res.status(200).send({
msg: "hello world!",
elaptime: Date.now() - reqTime,
});
});
app.listen(3000, () => console.log("listening on port 3000!"));
後は "npx webpack" コマンドを実行して webpack で build をし、出力されるファイル "./dist/main.js" を node コマンドで実行すればいい。
npx webpack
node dist/main.js
以降の項で上記の実装、webpack.config.js や.babelrc(babel.config.js)が上記のようになる理由について順にみていく。
JavaScript の歴史から Node.js とそのモジュール管理の仕組みについて理解する
JavaScript の歴史から見ていくと分かりやすいと思ったので歴史から Node.js とはについてみていく事にする。
まず歴史の全体感は25 年に渡る JavaScript の歴史のサイトが分かりやすいのでそちらを参照。この歴史の中で特に関係する部分としては、
・ 2009 年 Node.js の誕生
・ 2015 年 ES2015:大規模な ECMAScript のアップデート
の 2 つ。また、同時に JavaScript の version についても知っておく必要があるが、それはJavaScript Versionsに一覧が載っており分かりやすい。
年表を見ると分かるが 2009 年に Node.js が誕生する。一応経緯みたいな事も補足しておくと、発端は Chrome に搭載された V8Engine と呼ばれるものがとても良いという事で、これを Java とか Ruby、Python と同じようにサーバーサイドの言語として使おうとして Node.js が生まれた。
ここで JavaScript の歴史と JavaScript の version を並べてみると、
・1997-1999 年 ES1 ES2 ES3
・2009 年~ ES5
・2015 年~ ES6
・2016 年~ ES2016
のようになっており、Node.js が生まれた時(2009 年)の JavaScript の version は ES5(ECMAScript 5)と呼ばれるものである事が分かる。となると必然的にその当時の JavaScript の version で Node.js が実装される事になる。そして表題の import/export の話に関わるが、この ES5 のモジュール管理の方法は CommonJS という仕様で定義されており、それがまさしく
const http = require("http");
という require/module.export の構文。一方、import/export の構文は ES6(以降で使えるようになった)構文であり、本質的に ES5 の構文とは大きく違っている。
こういった事情があり、Node.js では import/export が使えず、 require/module.export という構文で実装する必要がある(※Node.js もES6 の機能を取り込むなど変化し続けておりES Modulesにも対応しているが、ES Module対応のための追加設定をしなければ、基本的にNode.jsは ES5 の仕様であるという意味)。
・参考:Node.js とは何か
Node.js を import/export のモジュール管理の仕組みで実装するとどうなるか?
上記の項で見てきたように、Node.js では そのモジュール管理の仕組みが require/module.export という構文で、import/export という構文とは全く違う。実際にそれを体感するためわざと Node.js で import/export の構文で実装してみる。実装したコードは以下の通りで、これを "node index.js" で実行してみると、
// src/index.js
import express from "express";
const app = express();
// 省略
app.listen(3000, () => console.log("listening on port 3000!"));
[root@localhost node-express]# node src/index.js
(node:9816) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
/root/workspace/node-express/src/index.js:1
import express from 'express';
^^^^^^
SyntaxError: Cannot use import statement outside a module
at ...
というようにエラーが出てしまう。一部のみに抜粋しているが、import 文の所(モジュール管理の実装)でエラーが出てしまう事が分かる。
Q:ではどうするのか? → A:コードを変換してしまえ!
上記の項で見たように Node.js の仕様として import/export の構文は使えないのでどうしようもないと思うかもしれないが、そんな事はなくソースコードを変換するという発想でどうにかしてしまう。
コードの変換をするために登場するのが Babel で、すごく簡単に言ってしまえば JavaScript の コード(構文)の version を変えるツール。Babelとはオープンソースのライブラリで、これを使うと実装されたコードの JavaScript の構文を変える事ができる。このツールはUsing Babelのページを見ると分かるように、様々な方法で実行する事ができるが、大抵はwebpackという build ツールに組み込まれる形で使われる事が多いようで、build を行う時に JavaScript の構文 を変えるという事が行われる。
これにより例えば、ES6(2015 年の JavaScript version)の構文(import/export の構文)で実装されたコードを ES5(2009 年の JavaScript version)の構文(require/module.export の構文)に変換する、という事ができる。これにより Node.js でも import/export の構文を使って実装ができるようになる。
※webpack は公式のページの Top の図が示すように、JavaScript とか CSS とかその他色々なものをバンドルして 1 つにまとめるのがツールの本質だが、そのバンドルしてまとめる時に JavaScript の構文の version を変えるという事が良く行われる。
と書かれているように、webpack は基本的にはモジュールのバンドラーだが、それを実行する際にいろいろな追加の処理を行え、その処理に Babel を取り込ませる事でバンドル(Build)の過程で JavaScript の構文の変換を実行できる。そのため、webpack × Babel の組み合わせがよく見られる。
コード変換の仕組みを導入する
コードの変換をするには、上記で見てきたように webpack × Babel の組み合わせで設定をしていけばいい。設定は公式のbabel-loaderに沿って行えばよい。
// webpack.config.js
module.exports = {
target: "node",
entry: "./src/index.js",
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
],
},
};
// .babelrc
{
"presets": ["@babel/preset-env"]
}
この設定を行った後に、webpack コマンドを実行してみると、
$ webpack
asset main.js 949 KiB [emitted] (name: main)
runtime modules 1.04 KiB 5 modules
cacheable modules 716 KiB
javascript modules 455 KiB 56 modules
json modules 261 KiB
...
15 modules
Done in 3.77s.
のように コマンドは成功(build 成功)する。build が成功すると "./dist/main.js" が作成されるがこれが build の成果物で、このファイルを "node dist/main.js" で実行してみると、
$ node dist/main.js
...
webpack://my-webpack-project/./src/index.js?:13
var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(req, res) {
^
ReferenceError: regeneratorRuntime is not defined
at eval (webpack://my-webpack-project/./src/index.js?:13:46)
...
のようになぜかエラーが出てしまう・・・。なぜこうなるのか?次の項で見ていく。
※補足として target: 'node' についてだが、これは webpack の公式ドキュメント(Targets)を読んでみると、
と書かれているように、Node.js の場合は target: 'node' の設定が必要なため設定している。
core-js と regenerator-runtime を使う
上記の項で出たエラー『regeneratorRuntime is not defined】は、 ES6 の構文で aysnc/await やgenerator 関数というものを ES5 に変換するために必要になる翻訳(ES6 の構文の〇〇は ES5 の構文だと △△ になる)を担うものがないために起こっている。
まずそもそも JavaScript には標準組み込みオブジェクトと呼ばれるものが存在し、ライブラリの依存なしに使える標準機能のようなものがある。例えば、 "Array.from('foo')" とかが使えるのはこの標準組み込みオブジェクトのおかげ。この標準組み込みオブジェクトも JavaScript の version が変わるごとに変わっており、ES6 にはあるが ES5 にはないものがある。例えば、 "String.replaceAll()" は ES2021 から使えるようになった標準組み込みオブジェクト。
そこでpolyfillが登場する。これは JavaScript の version 間の構文の差異を埋めてくれるもので、例えば、ES6 にある標準組み込みオブジェクトで実装したコードを ES5 のコードに変換する役割を担ってくれる。具体的に何をするかというと、Array.from ポリフィルにあるような形で、"Array.from()" を使わない場合の実装に Array.from を置き換えてくれる。
この polyfill だが、以前は@babel/polyfillというものがありこれを利用していた。ただ、公式のサイトの注意書きの通りこのライブラリは非推奨になり、代わりに "core-js/stable" と "regenerator-runtime/runtime" を使い構文の変換を行う。@babel/polyfillのページに書かれている通り、それぞれ、
core-js
polyfill ECMAScript featuresregenerator-runtime/runtime
transpiled generator functions
の役割がある。
実装としては Babel のサイトにあるように、index.js に
index.jsimport "core-js/stable";
import "regenerator-runtime/runtime";
のように import を追加するだけでよい。
この実装を追加した状態で webpack コマンドを実行して、build 後のファイルを "node dist/main.js" で実行してみると、
$ node dist/main.js
listening on port 3000!
のようにサーバの起動ができるようになる事が確認できる。
※『結論:どうすればいいか?』の index.js でDate.now(), Array.from, async/awaitなどを書いているがこれは意図的にそうしており、
Date.now(), Array.from
標準組み込みオブジェクトの ES5 への変換を確認するため(polyfill ECMAScript features)async/await
generator 関数や async/await の ES5 への変換を確認するため(use transpiled generator functions)
の意味合いで実装している。
・ 参考:@babel/polyfill
・ 参考:Babel7.4 で非推奨になった babel/polyfill の代替手段と設定方法
おまけ
Babel の設定
JavaScript configuration filesに書かれているように、Babel の Configuration は JavaScript でも書ける。その場合は以下のようになる。
// babel.config.js
module.exports = { presets: [["@babel/preset-env"]] };
フロントエンドでも同じ事をする必要がある
今回は Node.js(サーバサイド)について見てきたが、フロントエンドでも古いブラウザ(IE11)などでES6を使いたい場合は、同じように変換が必要で webpack × Babel で構文の変換を行う必要がある。
※『はじめに』で React・Vue・Angular では import/export が使えるという話をしたが、これはそれらの JavaScript のプロジェクトのセットアップでBabelが設定されているため。
まとめとして
今回はNode.jsでも import/export を使った実装をするためにはどうすればいいか?についてみてきた。次回はこのNode.jsで実装したコードに対して静的解析とコードフォーマッティング(ESLint・Prettier)の導入をやってみたいと思います。
__________________________________
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/