Open17

Node.js

high-ghigh-g

概要

2009年
V8 JavaScriptエンジン上に構築されたJavaScript実行環境の一つ
ライアン・ダールによって作られる

大きく目指す世界

  • フロントエンド、バックエンドをともにjsで開発
  • C10K問題の解消

Node.jsは下記と同様の目的を持つ。

ほとんどの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 がリリースされる。

high-ghigh-g

V8

Googleが開発するオープンソースのJIT Virtual Machine型のJavaScriptエンジンである。
名前はV型8気筒エンジンに由来している(同じく「V8」と略される)

ECMAScript (ECMA-262) 5th Edition準拠で、C++で記述されている。
スタンドアロンでの実行が可能なほか、C++で書かれたアプリケーションの一部として動作させることもできる。

high-ghigh-g

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

high-ghigh-g

深掘りメモ

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

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の違いによって動作しないことがあります。これらの互換性を確認し、更新する必要があります。

high-ghigh-g

2023/11/09
実践Node.js入門 第3章 からスタート

CommonJS

exports.xxxとmodule.exportsが存在
併用はできず、両方記述した場合、module.exportsが優先される。
その為、どちらか一方に統一したほうがよい。

モジュールはシングルトンとして読み込まれる
→バグに繋がりやすいポイント

requireは分割代入可能

const { aaa } = require('./calc')

ECMAScript Modules(ESM)

JavaScriptの標準として策定されたモジュール方式
元々、JSの役割的に小さなファイルが多く
グローバルなファイル管理でも間に合っていたが、
昨今の大規模JS開発とはニーズが合わなくなり策定された仕様

high-ghigh-g

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

high-ghigh-g

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しにいく

high-ghigh-g

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);
high-ghigh-g

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);
})();

high-ghigh-g

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

high-ghigh-g

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&param2=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);
});

high-ghigh-g

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;

high-ghigh-g

ルートハンドラとミドルウェア

ルートハンドラ → 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を取得できる様になる