Node.jsの基本

2019/03/08更新

目次

概要

Node.jsを学び始めた初期の頃、調べたり人に聞いたりしてもなかなかはっきりと理解できなかった根本的なことや概念的なことを中心にまとめておく。

インストール

Node.jsをインストールすると、nodeコマンドが使えるようになり、nodeコマンドに*.jsファイルを渡して実行すると、Node.jsプログラムが実行できる。

ここまでは、perlやrubyのプログラムをコマンドラインで実行するのとほぼ同じイメージである。Node.jsは「サーバーサイドJavaScript」などと言われることもあるが、それ自体はサーバーではなく、Apacheなどをすぐに置き換えるものではない。むしろ、サーバー用途よりもタスクランナーなどのローカル環境での用途の方が多いように思われる。

Cygwinには対応していないが、Windowsにインストールしたnode.exeはCygwin上から問題なく叩ける。ただ、まれにパス区切り文字の違いなどで、Cygwinからは正常に動作しないパッケージなどもある。

npmとpackage.json

npmは、Node.jsのパッケージマネージャで、Node.jsインストール時にnodeコマンドと一緒についてくる。package.jsonは、アプリの情報を記述するJSONファイルである。

npm-scripts

package.json内のscriptsというキーに、シェルコマンドのように命令を書いておくと、npm run ~という簡潔なコマンドでそれを実行できるようになる。テスト、ビルド、デプロイ、実行、停止、などに相当するコマンドを事前に登録しておくと便利に使うことができる。

"scripts": {
  "start": "NODE_ENV=production node app.js",
  "test": "npm run test:eslint && npm run test:mocha",
  "test:eslint": "eslint app.js",
  "test:mocha": "mocha test.js"
},

上記例では、npm startでapp.jsの起動、npm testで2つのテストサブタスクの実行ができる。

非同期コード

Node.jsは、シングルスレッドで動いているため、同期的な処理(他の処理をブロッキングする処理)を徹底的に嫌う。そのため、ほぼ全ての関数がAjaxのような非同期関数となっており、従来はcallback関数を引数に渡して、継続する処理を記述していた。

しかし、callback方式では、ネストが深くなるいわゆる“callback地獄”に陥りやすく、コードの可読性低下が問題となっていた。そこで、現在ではcallback方式に代わり、Promiseオブジェクトが標準化され、さらに、Promiseの簡潔な記述法として、async/awaitが採用されている(ECMAScript 2015の新機能(その1)を参照)。

// Promiseを返す非同期関数
const myPromiseFunc = () => { … };

// 従来の書き方
const myFunc1 = () => {
    myPromiseFunc().then((res) => {
        console.log(res); // resolve時の処理
    }, (err) => {
        console.log(err); // reject時の処理
    });
};

// async/awaitによる書き方
const myFunc2 = async () => {
    try {
        const res = await myPromiseFunc();
        console.log(res); // resolve時の処理
    } catch (err) {
        console.log(err); // reject時の処理
    }
};

モジュール

Node.jsでは、require()を使って他の*.jsファイルをモジュールとして実行して読み込むことができる。これにより、コードを簡単に分割することができる。

require()の引数には、そのファイルから対象のファイルへの相対パスを指定する。戻り値には対象のファイルがmodule.exportsに代入したオブジェクトが返る。

// my-module.js
const myModule = {};
myModule.myFuncA = () => { … };
  :
module.exports = myModule;

// index.js
const myModule = require('./my-module'); // .jsは省略可能。./は省略不可。
myModule.myFuncA();

require時に引数を渡したり、初期化処理を行ないたい場合は、module.exports自体を関数にすればよい。

// my-module.js
module.exports = (arg1, arg2) => {
      :
    // 初期化処理等
      :
    const myModule = {};
    myModule.myFuncA = () => { … };
     :
    return myModule;
};

// index.js
const myModule = require('./my-module')(x, y);
myModule.myFuncA();

モジュールは、require時に関数でラップされるので、最上位の関数外の場所にもreturn文を書くことができる。

環境変数

Node.jsでは、プログラム実行時の環境変数keyの値をprocess.env.keyで参照できる。Bash等のシェル上であれば、nodeコマンドの前にkey=valueを付けて実行することで、環境変数keyにvalueを割り当ててプログラムに渡すことができる。

特に、NODE_ENVという環境変数は、プログラムの実行環境を表すものとして「production」「staging」「development」「test」「local」などの値がよく指定される。

// 「NODE_ENV=production PORT=8080 node app.js」を実行した場合:
console.log(process.env.NODE_ENV);  // "production"
console.log(process.env.PORT);      // "8080" ※全てStringとして渡されるので注意。

相対パスの注意

相対的な位置にある他のファイル等を参照する場合は注意が必要。例えば、以下のようなディレクトリ構造となっていた場合を考える。

/app.js
/dir/hoge.js
/dir/fuga.js
/dir/data/data001

require()に指定する相対パスは、必ずそのファイルからの相対パスとなる。

// app.js内
const fuga = require('./dir/fuga');

// hoge.js内
const fuga = require('./fuga');

それ以外の相対パスは、nodeコマンドを実行したディレクトリからの相対パスとなる。

// hoge.js内
fs.readFile('data/data001', (err, data) => { ... });
// 「/dir/data/data001」を指定したつもりでも、
// 「/」にて「node app.js」と実行すると、「/data/data001」を探しにいってしまう。

__dirnameはそのファイルが置かれたディレクトリを表す。path.join()でつなげることで、正しい相対パスが得られる。

// hoge.js内
const path = require('path');
fs.readFile(path.join(__dirname, 'data/data001'), (err, data) => { … });
// 「/dir/data/data001」が正しく読み込まれる。

Node.jsによるウェブサーバー

以下のようなHTTPサーバーを作ってリクエスト待ちを開始するコードを書いて実行すると、ウェブサーバーとなる。ただし、このようなHelloWorldコードでは、どんなリクエストに対しても同じ応答を返すだけで、Apacheサーバーのようにパスに応じてファイルを返したりCGIを実行したりするような複雑なことは全くできない。

const http = require('http');
http.createServer((req, res) => {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end('Hello, world\n');
}).listen(8080);

このように、Node.js自体はサーバーではないため、ウェブアプリを作る場合はサーバー機能も含めて実装する必要がある。Express.jsは、Node.jsの代表的なウェブアプリ構築用フレームワークの一つ(デファクトスタンダード)で、パスによるルーティングなどのサーバー機能を含めたウェブアプリを簡単に実現することができる。

ウェブアプリにおける排他制御

Node.jsでは同期的な処理を徹底的に嫌うと書いたが、同期的な処理が必要となる場面もある。

例えば、古典的なアクセスカウンタのように、ファイルから数字を読み取り、1を加算して再び同じファイルに書き込むという処理を行なうサーバープログラムを作るとする。この処理はデータベースで言うところのトランザクションであり、原子性(Atomicity)が確保されなければならない。

このようなケースにおけるファイル入出力を非同期処理で書いた場合、アクセスが集中すると、複数のリクエストが同じ数字を読み込んでしまったり、読み込みや書き込みに失敗してしまう事態が簡単に発生する。ファイル入出力の非同期処理中に、他のリクエストを次々と受け取ってしまうためである。

このような場合は、非同期ではなく同期的に処理を行なわなければならない。例えば、ファイル読み込みであれば、通常はfsモジュールのreadFile()を使うが、同期的にファイルを読み込めるreadFileSync()という関数も存在するので、これを使う。これで、上記のような問題は回避できる。ただし、Node.jsの特長を十分生かせず、パフォーマンスが犠牲になってしまう可能性はある。

const app = require('express')();

// 非同期的(非排他的)なファイル読み書き
app.get('/', (req, res) => {
    // 非同期処理を待っている間に、他のGETリクエストの処理が始まってしまうため、複数のリクエストが同じ値を読み込んでしまう。
    fs.readFile('data.txt', { encoding: 'utf-8' }, (err, data) => {
        data = String(parseInt(data, 10) + 1);
        fs.writeFile('data.txt', data, { encoding: 'utf-8' }, (err) => {
            res.json({ success: true });
        });
    });
});
// 例えば、100のリクエストが同時に集中した場合、data.txtの数値は10~20くらいしか増えていない。
// もしくは読み込み失敗が発生してNaNになってしまう。

// 同期的(排他的)なファイル読み書き
app.get('/', (req, res) => {
    // ファイルI/Oの最中もブロックされるため、他のリクエストの処理が始まることなく、処理の順序が保証される。
    let data = fs.readFileSync('data.txt', { encoding: 'utf-8' });
    data = String(parseInt(data, 10) + 1);
    fs.writeFileSync('data.txt', data, { encoding: 'utf-8' });
    res.json({ success: true });
});
// 100のリクエストが同時に集中した場合でも、data.txtの数値は正確に100増えている。

関連記事