Node.jsこと始め(備忘録)

なんとなく使っていたNode.jsって??と漠然とした疑問
疑問を解消すべく、調べました

ついでに、これからはバージョン管理をします

目次
  1. インストールについて(バージョン管理)
  2. Node.jsとは
  3. package.json
    1. package.jsonの内容から一括でパッケージをインストール
    2. 個別にパッケージをインストール
  4. npxについて
  5. コードの実行
  6. モジュール
    1. Node.jsのモジュール
    2. require
    3. module.exports
  7. 追記:Node.jsでESmodulesを使うとき
  8. グローバル
  9. コアモジュール
    1. pathモジュール
    2. fsモジュール
    3. httpモジュール
  10. console.logでの深いオブジェクトの表示
  11. まとめ

インストールについて(バージョン管理)

前にインストールしたNode.jsは、バージョンが古くなってきたので、バージョン管理をします
(/usr/local/bin/にあるnodeはとりあえずはそのままにしておきます^^;)
バージョン管理に「nvm」を使用します
「nvm」はユーザーごとにインストールされ、シェルごとに呼び出される設計です


*インストール後、ターミナルは再起動します

# Node.jsのリリースされているバージョンをすべて列挙
nvm ls-remote

# インストール
nvm install バージョン

# LTS(長期サポート)な最新バージョンをインストール
nvm install --lts

# LTSを最新にしたときに現在グローバルにインストールしているモジュールもインストール
nvm install "lts/*" --reinstall-packages-from=current

# インストール済のバージョンを確認
nvm ls

# バージョンを切り替える
nvm use バージョン

# 現在使用しているバージョンを表示
nvm current

# 特定バージョンのアンインストール
nvm uninstall バージョン

Node.jsの偶数バージョンは「LTS」と呼ばれ、長期サポートが保証されています

余談グローバルインストールとローカルインストールについて
npm install モジュール名 -g 
「-g」オプションをつけると「通常は/usr/local/bin/:パスが通っている場所にパッケージをインストール(グローバルにインストール)します
「-g」をつけなければ、ローカル(現在のプロジェクトだけ)にインストールします
*通常のモジュラーはバージョン依存が大きいため、ローカル環境へインストールするほうがいい

「npm」については、あとの「package.json」でざっくりと^^;

(余談)/usr/local/bin/は通常のダウンロードでセットアップしたNode.js本体がある場所です

Node.jsとは

Node.jsはJavaScriptを実行するための環境です
*その他のJavaScrip実行環境にはDenoやBunなどがある

Node.js はスケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動JavaScript 環境です

https://nodejs.org/ja/about/

「JavaScript環境です」とは?

JavaScriptは言語名、Node.jsはJavaScriptの処理系です
処理系とは、その言語を解釈して「0と1」だけの機械語に翻訳(コンパイル)するためのソフトウェアです
*Node.jsはV8エンジンを使用

ちなみに、ブラウザにはJavaScriptの処理系が入っているので、インストールすることなくJavaScriptが実行できます
*ChromeブラウザはV8エンジンを使用

JavaScriptは実行時にコンパイルします

Node.jsには「JavaScriptの実行環境」以外に「npm(JavaScriptのライブラリ管理ツール)」が用意されています

「ネットワークアプリケーションを構築するために設計された」とは?

Node.jsは「Webサーバーソフトウェア(Webサーバー)」や「アプリケーションサーバーソフトウェア(APサーバー)」として利用できます
それってメチャクチャ難しそう💦

*他の言語は通常APサーバーが必須、PHPはApacheと仲良しなので特別!!

ミドルウェア
ミドルウェアはアプリケーションとOSの間で両者の機能を補佐します
ミドルウェアが存在することで、複雑な動作が可能です
例えば、HTTPリクエストを解釈してプログラムを呼び出す役割が、APサーバーです
Web3層アーキテクチャ
2層のクライアントサーバーモデルを、負荷分散や柔軟なシステム変更のために、3階層に分割したシステム
プレゼンテーション層
HTTPリクエストの送受信とアプリケーション層の呼び出し
アプリケーション層
ビジネスロジック担当、プレゼンテーション層(画面)に入力された値をもとに処理を行い、必要があれば「データ層」に要求します
データ層
「アプリケーション層」からデータ要求があれば、データベースを探索、結果をアプリケーション層に返します

「スケーラブルなネットワークアプリケーションを構築するため」というのは、C10K問題が関係しているようです

C10K問題(英語: C10K problem)とは、Apache HTTP ServerなどのWebサーバソフトウェアとクライアントの通信において、クライアントが約1万台に達すると、Webサーバーのハードウェア性能に余裕があるにも関わらず、レスポンス性能が大きく下がる問題である。

原因
この現象の原因は主に2019年現在も広く用いられているApache HTTP Serverの駆動方式にある

回避方法
C10K問題を回避するためには様々な方法がある。

Nginxなど、C10K問題が起こらないように設計されたソフトウェアを使用する
サーバーサイドでNode.jsなどの駆動方式を持ったソフトウェアを使用する

https://ja.wikipedia.org/wiki/C10K%E5%95%8F%E9%A1%8C

非同期型のとは?

Node.jsは「シングルスレッド」「ノンブロッキングI/O」です
シングルスレッドでは、前の処理が終わらないと次の処理が始まらない
しかし、重い処理が終わるまでの間は、実行中のプログラムは非同期にし(遅延実行して)待ち時間で別の処理ができます
マルチスレッドでは、前の処理を待たずに別スレッドが立ち上がります
しかし、ブロッキングI/O(同期処理)で、順番に処理するため実行中のプログラムには待ち時間が発生します

シングルスレッドのイメージ図

プログラムを上から順番に実行したのでは、正しい結果にならない場合があります
その場合、JavaScriptでは、時間のかかる処理を待ってからする処理(非同期処理)が重要です

イベント駆動とは?

フロー駆動
上から順番に実行されるプログラムです
イベント駆動
イベントが発生すると実行するプログラムです
裏側では無限ループでイベントの発生を監視しています(この無限ループをイベントループといいます)

package.json

Node.jsをローカル開発で利用
「npmレジストリ」とは、世界中の開発者が作った Node.jsのパッケージが集められた場所です
「npmレジストリ」からパッケージをインストールして利用します

通常「Node.jsプロジェクト」を1つ作る時は「npmパッケージ」を1つ作ります
パッケージを管理するためのファイルが「package.json」です
プロジェクトのフォルダに移動してnpm init -y (-yは質問を省略するオプションです)で「package.json」を作成します

パッケージ管理が必要な理由は、パッケージ間の複雑な依存関係です

1つのライブラリーが依存するパッケージは膨大な数で依存関係はカオス状態です
すべての依存パッケージは、ローカルのルートディレクトリ直下に作成される「node_modulesディレクトリ」にダウンロードされます(この場所を編集することは、ほぼありません)
仮置場のようなものです(通常、数百MBもの容量を使います)

パッケージをインストールすると、「package-lock.json」にすべての「依存パッケージバージョン」が自動で追加されます
「package-lock.json」はバージョンを固定するために必要です

package.jsonの内容から一括でパッケージをインストール

npm installを実行する

「package-lock.json」が存在しないとき
「package.json」に基づいてdependency (dependencyとは、そのパッケージが使用している他のパッケージ)がインストールされ、実際にインストールされたすべてのパッケージバージョンが「package-lock.json」に書かれます
「package-lock.json」が存在するとき
package-lock.json」に基づいてインストールします
しかし、「package.json」指定されたバージョンとの矛盾があれば、「package.json」が優先され、実際にインストールされたバージョンが「package-lock.json」に書かれます
「package-lock.json」をもとにバージョンを完全に固定
npm ciを実行します

ちなみに、依存関係のバージョンが原因でエラーになった場合、「node_modules」と「package-lock.json」を削除してから、原因となったパッケージのバージョンを切り替えてnpm installを試します

またはキャッシュデータが何らかの形で破損していた、もしくは不整合が生じていた可能性が高い場合は
npm cache clean --force
して新しいキャッシュが生成され、問題が解消されることがあります

scripts(npm-scripts)について
*右側コマンドを左側のエイリアスに置き換えて実行します
npm-scriptsはnode_modules内のパスを書かずにインストールしたパッケージのコマンドを実行できます

npm run エイリアス で実行します

“preコマンド名”: で前処理・“postコマンド名”で後処理ができます
(例:npm run buildで、./dist/以下のファイルとフォルドを削除して、ビルドを実行、finishedを表示します)

&で接続することで並列処理、&&では直列処理を行うこともできます

"scripts": {
   "prebuild":"rm -rf ./dist/*",
    "build": "webpack --mode production",
    "postbuild": "echo finished",
    "dev": "webpack-dev-server --mode development",
  }

個別にパッケージをインストール

npmのバージョン5からデフォルトの挙動となったので、現在は明示的に–saveオプションを指定する必要はありませんが、–saveはインストールしたパッケージの情報をpackage.jsonのdependenciesに登録するオプションです

npm install --save パッケージ名

開発環境のみで利用して、本番環境では利用しない用途でインストールする(「devDependencies」でのインストール)

npm install --save-dev パッケージ名
npm i -D パッケージ名

*dependenciesはプロジェクトを動作させるために必要なパッケージ、devDependenciesはが開発時において使用するツールなどのパッケージを登録(例としては、babel・webpack・eslint・prettier・jestなど)

バージョンを指定してインストール

npm install パッケージ名@バージョン

バージョンの^は、左から最初に出現した0以外の数字のバージョンを固定する
*メジャーバージョンは固定(後方互換は担保)してその中の最新をインストール

npxについて

Node.jsをインストールすると、npxというコマンドも一緒に提供されます。
このnpxは、npmパッケージ管理ツールの一部で、npmバージョン5.2.0以降で利用可能です。

npxの特徴として、以下の順番でコマンドを探し、実行する能力があります
1npxは現在のディレクトリのnode_modules/.binを探します。
2次に、システム全体のパス(環境変数PATH)を探します。
3最後に、それらの場所でコマンドが見つからない場合、npxはnpmレジストリからパッケージを一時的にダウンロードして実行します。
その結果、npxを使用すれば、npmパッケージをグローバルにインストールせずに、一度だけ実行することが可能です。また、一時的にダウンロードしたパッケージはキャッシュされ、再度同じコマンドを実行する際にダウンロード時間を節約することができます。

コマンド名とパッケージ名が異なる場合や、特定のバージョンのパッケージを指定する場合は、-pオプションを使ってパッケージ名を明示します。また、@で始まるスコープ付きパッケージに対しても使用できます。

余談:環境変数PATHに含まれるコマンドもnpx経由で実行できます。これは、npxがコマンドを探す際にPATHを検索するためです。

コードの実行

絶対パス /
相対パス ./../親 又は/なし

node ファイルパス

例:プロジェクトのディレクトリ直下にmain.jsを作成した場合
カレントディレクトリがブロジェクトディレクトリなら

node main.js

プログラムを途中で終わる場合

process.exit()

プログラムの終了と終了ステータス
プログラムが最後まで達すれば、 exit を使わなくてもプログラムは終了し、終了ステータスは 「0 」です
正常終了でなければ、適切な終了ステータスを設定するために exit(1) で終わるべきです

process.exit(1)

VSCodeで実行

「実行」→「デバック開始」→「Node.js」
または「実行」→「F5キー」→「Node.js」

余談:デバックについて

ブレイクポイントを設定
ステップオーバー:関数の中に入らずに順番に実行
ステップイン:関数の中に入って実行
ステップアウト:(ステップインの後)関数の呼び出し元の次の関数に行く

モジュール

モジュールとは、使い道を決めて「処理をひとまとまりにして、切り出したもの」です
モジュール化することで、モジュールの再利用やモジュール単位での機能交換など、効率的になります

Node.jsのモジュール

Node.jsのモジュールは、3パターンです

  • 自作モジュール
  • npmパッケージ(インストールして利用します)
  • コアモジュール (Node.jsに組み込みまれているモジュール:インストールせずに利用します)

require

「require」メソッドは、モジュールを読み込むときに使います(モジュールを使う側)

requireメソッドは、エクスポートされたモジュールを戻り値として返します
戻り値を定数に代入し、その定数を使うことでモジュールを扱えます

const obj = require("モジュールのパスまたはモジュール名")

引数は
「自作モジュール」の場合は「モジュールのパス」
「npmパッケージ」や「コアモジュール」の場合は「モジュール名」です

*「require」で一度呼ばれたファイルはキャッシュされます

module.exports

モジュールを切り出すときは、「module.exports」を使います(モジュールにするファイルで使います)

モジュールを作成する場合
切り出しは、「module.exports」や「exports」を使用します
「module.exports」と「exports」の違いは?

「moduleオブジェクト」はグローバルオブジェクトです
「moduleオブジェクト」は「exportsプロパティ」を持っていて同じオブジェクトを参照しています
「module.exports」の場合、module.exports = {...} のように利用したい値を格納できます

しかし「exports」の場合、exports={...} ではmodule.exportsとは異なる新しいオブジェクトを作っただけです
exportsの場合は、任意のプロパティ名を設定して値を格納します
exports.hoge = hoge

基本的には「module.exports」を利用して、「複数の値をまとめたオブジェクト」で切り出すのかなと思います^^;

const hoge = function(){
  //処理内容
};

module.exports = {
  name: "太郎",
  hoge,
};

*現在はESmodulesへ移行途中

追記:Node.jsでESmodulesを使うとき

絶対パスでモジュールを読み込みたい
「require」関数を作ります

import module from 'module';
const require = module.createRequire(import.meta.url);

const hogehoge = require("絶対パス");

そのままでは__dirname(現在のディレクトリのパス)が使えないので

import { dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url));

グローバル

ブラウザとの比較

グローバルオブジェクトグローバル変数の利用
Node.jsglobalモジュール内のどこでも利用可能
モジュール間では利用不可能
ブラウザwindowモジュール内、間どちらも利用可能

*globalThis:その環境のグローバルオブジェクトを取得します

グローバルオブジェクトのプロパティ・メソッドの一例 
どこからでも利用でき、利用時にglobalは省略できます

__dirname
実行中ソースコードの ディレクトリパスを取得
__filename
実行中ソースコードの ファイルパスを取得
module / exports / requite()
モジュール関連
process
実行環境に関する情報
使用例 
「.envファイル」で環境変数とその値を指定して
presess.env.定数名で値を取得します

process.argvはコマンドライン引数を取得します
argv[0] : node実行ファイルのフルパス
argv[1] :スクリプトファイル (.js) のフルパス
argv[2]: 1つ目の引数

コアモジュール

コアモジュールは、Node.jsに組み込みまれているモジュールです
コアモジュールはインストールせずに、利用します

下記のコマンドは、コアモジュールを一覧で表示します

node -e "console.log(require('module').builtinModules);"

pathモジュール

pathモジュールはファイルパスからディレクトリ名やファイル名を文字列として取得します

const path = require('path')

// ディレクトリ名を取得  /a/b
path.dirname('/a/b/c.txt')

// ファイル名を取得  c.txt
path.basename('/a/b/c.txt')

//拡張子を取得 .html
path.extname('index.html')

//パス文字列の結合
path.join('','',...)

//絶対パスを返します
path.resolve('','',...)

*パス表記にはPOSIXスタイル(”/”区切り)とWindowsスタイル(”¥”区切り)があり、デフォルトで実行環境に依存します

fsモジュール

すべてのFile system操作には、Promiseベース・コールバック・同期があります

sample.txtファイルを読み取り、copy.txtに書き込みをします(非同期処理します)
fsモジュールの「readFile」メソッドは、非同期ファイルを読み込み
fs.readFile(ファイルのパス, 文字コード, コールバック関数)
「writeFile」メソッドは、非同期でファイルを書き込みます
fs.writeFile(ファイルのパス, ファイルの中身, コールバック関数)
*ファイルのパスに相対パスを指定して実行すると、実行したディレクトリによってパスが異なります
*文字コードはデフォルトがutf8
今回は「util.promisify」でプロミス化していますが
注意import fs from "fs/promises";でインポートすればプロミス化する必要はありません

const fs = require("fs");
const path = require("path");
const util = require("util");

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

readFile(path.join(__dirname, "sample.txt"), "utf8")
.then(data => {
  return writeFile(path.join(__dirname, "copy.txt"), data);
})
.catch(err => {
 console.log(err.message);
});

*重たいファイルを読み込んで処理する場合はストリームを使います(ストリームは小分けにして処理する仕組みです)

httpモジュール

簡単なHTTPサーバー

const http = require('http');
const server = http.createServer((request, response)=>{
  response.end('hello');
});
server.listen(3000);

console.logでの深いオブジェクトの表示

Node.jsの仕様で、深い階層にあるオブジェクトを「console.log」で表示すると、省略表示([Object])になってしまいます

第一引数:文字列化したいオブジェクト
第二引数:non-enumerableプロパティも表示するかどうかの指定(デフォルトはfalse)
第三引数:オブジェクトの階層(デフォルトは2で、nullにするとどんなに深くてもすべて表示)

 const util = require('util');
 console.log( util.inspect(オブジェクト, false, null));

まとめ

Node.jsで、一般的なWeb開発タスクは直接サポートされていないため
通常はフレームワークを利用します
クライアントサイドのアプリケーションでは「Electron」
サーバーサイドアプリケーションでは「Express」が有名です