Node.js

モチベーション
- Node.js周辺の基礎理解
- package.jsonの理解
- フロントエンド側の利用
- バックエンド側の利用
参考

概要
2009年
V8 JavaScriptエンジン上に構築されたJavaScript実行環境の一つ
ライアン・ダールによって作られる
大きく目指す世界
- フロントエンド、バックエンドをともにjsで開発
- C10K問題の解消
Node.jsは下記と同様の目的を持つ。
- PythonのTwisted
- PerlのPOE
- C言語のlibevent(英語版)https://blog.kgwr.net/libevent-02/
- RubyのEventMachine
ほとんどのJavaScriptとは異なり、ウェブブラウザの中で実行されるのではなく、むしろサーバサイドJavaScriptの一種
Node.jsを用いた構成としてはMEAN, MERN等が提唱されている。
年表
2009年:
Ryan Dahl によって Node.js が初めて公開される。
2010年:
npm (Node Package Manager) が公開される。これにより、Node.js のライブラリやフレームワークを簡単にインストール・管理できるようになる。
2011年:
企業が Node.js を採用し始める。LinkedIn, Walmart などがその先駆けとなる。
2012年:
Node.js がますます人気を集め、多くの大規模なアプリケーションで使用されるようになる。
2013年:
TJ Holowaychuk が Express.js のメンテナンスを StrongLoop に移譲。
2014年:
IO.js が Node.js のフォークとして生まれる。このフォークはコミュニティの主導によるもので、新しい機能の追加やバージョンアップのペースを速めることを目的としていた。
2015年:
Node.js と IO.js が和解し、Node.js Foundation のもとで統合される。
Node.js v4 がリリースされる。これは Node.js と IO.js の統合版としてリリースされたものである。
2016年:
Node.js v6 がリリースされる。このバージョンは長期サポート(LTS)を受けることが決定されている。
2017年:
npm の利用者数が増加し、JavaScript のエコシステムが急成長。
Node.js v8 がリリースされる。このバージョンには V8 エンジンの新しいバージョンや async/await といった新機能が追加されている。
2018年:
Node.js v10 がリリースされる。
npm, Inc. が Microsoft に買収される。
2019年:
Node.js v12 がリリースされる。
2020年:
Node.js v14 がリリースされる。
2021年:
Node.js v16 がリリースされる。
2022年
Node.js v18 がリリースされる。
2023年
Node.js v20 がリリースされる。

V8
Googleが開発するオープンソースのJIT Virtual Machine型のJavaScriptエンジンである。
名前はV型8気筒エンジンに由来している(同じく「V8」と略される)
ECMAScript (ECMA-262) 5th Edition準拠で、C++で記述されている。
スタンドアロンでの実行が可能なほか、C++で書かれたアプリケーションの一部として動作させることもできる。

各 JavaScript Runtime で使われている技術の違い
Node.js: C++, V8
Deno: Rust, V8
Bun: Zig, JavaScriptCore (Webkit)

Node.jsの内部仕様を知る
とほほを読む
そこまで情報量なかった...
公式を読んでいく
Node.jsについて
サーバサイドJSとバンドラーの利用用途を分けたほうが良いかも
知りたいのはどれくらいNode.jsに負債が積み上がってるか

深掘りメモ
- Node.jsの独自仕様、標準Web仕様具合
- Bunの独自仕様、標準Web仕様具合
- npm, yarn, pnpm, bunの各種インストール時のnode_modules内の構造
- ビルド
- webpack, vite, esbuild, Bun.buildの比較
- サーバサイドJSとして利用したとき
- Nodeの問題点

ChatGPTに聞いたNode.jsの負債となりえる仕様
シングルスレッド:
Node.jsはシングルスレッドで動作します。これは、CPUバウンドのタスク(計算処理が重いタスク)が多い場合、アプリケーションのパフォーマンスを阻害する可能性があることを意味します。
コールバック地獄:
早期のNode.jsの非同期処理はコールバックベースでした。これは、多くの非同期処理を連鎖させるとコードが複雑になりがちな「コールバック地獄」という問題を生む可能性がありました。近年はasync/awaitの導入によってこの問題は緩和されています。
NPMの脆弱性:
NPM(Node Package Manager)を通じてインストールされる多数のサードパーティーライブラリには、セキュリティ脆弱性や非互換性の問題が発生することがあります。依存関係を定期的に更新・監査することが必要です。
メモリ使用量:
大量のユーザーやリクエストを扱う場合、Node.jsのアプリケーションはメモリを多く消費することがあります。適切なメモリ管理やガベージコレクションの最適化が重要です。
バージョンの非互換性:
Node.jsのメジャーバージョンアップに伴い、破壊的変更が導入されることがあります。これにより、既存のコードベースが新しいバージョンと非互換になることも。
エラーハンドリング:
非同期処理におけるエラーハンドリングは、特に注意が必要です。エラーが適切にキャッチされない場合、全体のアプリケーションが停止することもあります。
Nativeモジュール:
C++で書かれたNativeモジュールは、Node.jsのバージョンアップやOSの違いによって動作しないことがあります。これらの互換性を確認し、更新する必要があります。

Node.js シングルスレッドについて
色々記事を見つけたけど、実践Node.js入門を読む

2023/11/09
実践Node.js入門 第3章 からスタート
CommonJS
exports.xxxとmodule.exportsが存在
併用はできず、両方記述した場合、module.exportsが優先される。
その為、どちらか一方に統一したほうがよい。
モジュールはシングルトンとして読み込まれる
→バグに繋がりやすいポイント
requireは分割代入可能
const { aaa } = require('./calc')
ECMAScript Modules(ESM)
JavaScriptの標準として策定されたモジュール方式
元々、JSの役割的に小さなファイルが多く
グローバルなファイル管理でも間に合っていたが、
昨今の大規模JS開発とはニーズが合わなくなり策定された仕様

2024/08/07
ブラウザとNode.jsの実行環境の違い
ブラウザ:ESM
Node.js:CJS, ESM
機能面
ブラウザ:document, alert
Node.js:require, fs, path
グローバルオブジェクト
ブラウザ:window
Node.js:global
globalThis:新しいESMで定義されたキーワード。適宜グローバルオブジェクトを返却するキーワード
windowやglobalのかき分けが面倒なときはglobalThisを利用すればok

2024/08/13
name packageのid
version バージョン
keywords npm wordsでの検索ワード
homepage packageのホームページ。npm docsやnpm homeでサイトを開くことが出来る
repository リポジトリ名
開発をする時に必要なのは、dependencies, devDependencies, scriptsが主だったもの
import ◯ from '■' と記述した場合、node_modulesのフォルダ名と一致するものを検索しにいく
対象パッケージのディレクトリ直下のpackage.jsonに記載されているmainの項目が本体
import ◯ from 'lodash/cloneDeep.js' とした場合、cloneDeepを直接importしにいく

2024/08/14
Playwrightを利用したスクレイピング
import { chromium } from '@playwright/test';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000');
const htmlStr = await page.content();
console.log(htmlStr);
DOMから文字列を取得する
const browser = await chromium.launch({ headless: false, slowMo: 500 });
const page = await browser.newPage();
await page.goto('http://localhost:3000');
const pageTitleLocator = await page.locator('.navbar-brand');
const pageTitle = await pageTitleLocator.innerText();
console.log(pageTitle);

2024/08/15
locator続き
クラスなどの手がかりがない場合、XPathでlocatorを取得できる
const inputLocator = page.locator('//*[@id="__next"]/div/div[1]/label/input');
await page.waitForTimeout(1000);
await inputLocator.type('テスト');
ブラウザを立ち上げ、フォームにテキストを入力後、pagerをクリック後、表示されている情報をカウントするスクリプト
import { chromium } from '@playwright/test';
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 500 });
const page = await browser.newPage();
await page.goto('http://localhost:3000');
const inputLocator = page.locator('//*[@id="__next"]/div/div[1]/label/input');
await page.waitForTimeout(1000);
await inputLocator.type('美');
const pager3Locator = page.locator('.page-link.page-number >> nth=2');
await pager3Locator.click();
const cardLocator = await page.locator('.cards.list-group-item');
const cardCount = await cardLocator.count();
console.log(cardCount);
})();

2024/08/16
Nodeでhttpサーバーの実装
import * as http from 'http'
httpはnode.js標準パッケージ
httpパッケージのcreateServerメソッドを実行し、
インスタンスをlisten()することで、簡易的にWebサーバを立ち上げることが出来る。
コールバック関数内で、res.end()に値を渡すことで、ブラウザにレスポンスを返すことが出来る。
立ち上げたサーバに対し、ブラウザからアクセスすると、res.end()内に記述した文字列が表示される。
import * as http from 'http';
const server = http.createServer((req, res) => {
res.end('Hello, http server!');
});
server.listen(8080);
content-type:
サーバからブラウザへ返却する文字列のフォーマットと文字コード
res.writeHeadを利用する
res.writeHead(200, { 'content-type': 'text/html;charset=utf-8' });
htmlのmetaタグで設定してもok

2024/08/21
http.createServer()の役割をexpressを利用すると、以下で書くことが可能
import express from 'express'
const app = express()
app.listen(8080, () => {
console.log(`Server start: http://localhost:8080`);
});
http.createServer内でres.writeHeadやres.write, res.endなどで記述していたものは、
expressの場合、すべてres.sendでまとめることが出来る。
res.sendを続けて2度読んだ場合は、res.end()と同じくエラーになる。
app.get('/', (req, res) => {
res.send(`
<a href="/result?param1=1¶m2=2">Get Method Link</a>
<form action="/result" method="POST">
<input type="text" name="title">
<input type="text" name="description">
<input type="submit">
</form>
`);
});
expressの場合、クエリストリングの加工は自動で行ってくれている為、req.urlに対するsplitやURLSearchPramsなどは不要
app.get('/result', (req, res) => {
const params = req.query;
console.log(params);
});
postを記述する場合
// postを利用する場合に必須の記述
// postの本文の内容をreq.bodyで取得できるようにする(これがないとbodyが空になる)
app.use(express.urlencoded({ extended: true }));
// postもgetと同じくメソッド形式で記述
app.post('/result', (req, res) => {
const params = req.body;
console.log(params);
});

2024/08/22木
jsonでレスポンスを渡す
res.sendでもres.jsonでもok
app.get('/', function (req, res) {
res.json({ message: 'hello', number: 1, array: ['banana', 'orange', 1] });
});
jsonをpostで受け取りたい場合、app.use(express.json())
を追加する
app.use(express.json());
app.get('/', function (req, res) {
res.send(`
<form action="/result" method="POST">
<input type="text" name="title">
<input type="text" name="description">
<input type="submit">
</form>
<script>
const formEl = document.querySelector('form')
formEl.onsubmit = (e) => {
e.preventDefault()
const title = formEl[0].value
const description = formEl[1].value
const res = await fetch('/result', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, description })
})
console.log(await res.json())
}
</script>
`);
});
app.post('/result', function (req, res) {
const params = req.body;
console.log(params);
res.json({ msg: 'success' });
});
app.listen(PORT, function () {
console.log(`Server start: http://localhost:${PORT}`);
});
RESTでAPIを作る
import express from 'express';
const PORT = 8080;
const app = express();
app.use(express.json());
const products = [
{ name: 'table', price: 1000 },
{ name: 'chair', price: 100 },
{ name: 'clock', price: 700 },
];
app.get('/products', function (req, res) {
res.json(products);
});
app.get('/products/:id', function (req, res) {
res.json(products[req.params.id]);
});
app.post('/products', (req, res) => {
const newProduct = req.body;
products.push(newProduct);
console.log(products);
res.json(newProduct);
});
app.delete('/products/:id', (req, res) => {
const deleteId = req.params.id;
products.splice(deleteId, 1);
console.log(products);
res.json({ deleteId });
});
app.patch('/products/:id', (req, res) => {
const targetProduct = products[req.params.id];
if (req.body.hasOwnProperty('name')) {
targetProduct.name = req.body.name;
}
if (req.body.hasOwnProperty('price')) {
targetProduct.price = req.body.price;
}
console.log(products);
res.json(targetProduct);
});
app.listen(PORT, function () {
console.log(`Server start: http://localhost:${PORT}`);
});
appで直接記述していたapiはrouterとしてまとめることが出来る。
import express from 'express';
const router = express.Router();
const products = [
{ name: 'table', price: 1000 },
{ name: 'chair', price: 100 },
{ name: 'clock', price: 700 },
];
router.get('/', function (req, res) {
res.json(products);
});
router.get('/:id', function (req, res) {
res.json(products[req.params.id]);
});
router.post('/', (req, res) => {
const newProduct = req.body;
products.push(newProduct);
console.log(products);
res.json(newProduct);
});
router.delete('/:id', (req, res) => {
const deleteId = req.params.id;
products.splice(deleteId, 1);
console.log(products);
res.json({ deleteId });
});
router.patch('/:id', (req, res) => {
const targetProduct = products[req.params.id];
if (req.body.hasOwnProperty('name')) {
targetProduct.name = req.body.name;
}
if (req.body.hasOwnProperty('price')) {
targetProduct.price = req.body.price;
}
console.log(products);
res.json(targetProduct);
});
export default router;
app.useでapiパスの共通部分をまとめることが出来る
import express from 'express';
import apiRoutes from './api-routes/index.mjs';
const PORT = 8080;
const app = express();
app.use(express.json());
app.use('/api', apiRoutes); // ここ
app.listen(PORT, function () {
console.log(`Server start: http://localhost:${PORT}`);
});
apiの階層化も可能
import express from 'express';
import productRoutes from './products.mjs';
const apiRoutes = express.Router();
apiRoutes.use('/products', productRoutes);
export default apiRoutes;

ルートハンドラとミドルウェア
ルートハンドラ → GETやPOSTなどAPI系の制御を行う処理
ミドルウェア→ルートハンドラの前後で実行する処理。記述自体はルートハンドラと大差ない。
ミドルウェア
- useメソッドを利用する
- request → middleware → route handler という実行順序
- ルートハンドラに到達する前に呼ばれることで機能する
- ミドルウェアは複数登録することが出来る
- 全体の前に登録することも出来るし、特定のパスに対して登録することも出来る
- express.jsonの場合、POSTのbodyをjsonオブジェクトとして利用することが出来る
app.use(express.json())
- ミドルウェアの実行順序は第三引数のnext()を使用する
app.use('/', (req, res, next) => {
console.log('/ start');
next(); // 次の処理に進む
});
app.use('/', (req, res, next) => {
console.log('/ middle');
next(); // 次の処理に進む
});
app.get('/', function (req, res) {
console.log('/ get');
});
Server start: http://localhost:8080
/ start
/ middle
/ get
ルートハンドラであっても同じ用にnext()は利用できる
middlewareは前方一致
ルートハンドラは完全一致
next()の下の行に処理を書くと、処理の順序がややこしくなる為、基本は書かない
条件分岐で書く場合は、if + return next()を書く
res.send()下でnext()が実行された場合、2度以上res.send()が実行されるようなバグの温床になるため、next()とres.send()は隣り合わせにしない
useの引数を4つにすると、第1引数がerrorを取得できる様になる