banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

青訓キャンプ |「Node.js とフロントエンド開発の実践」

本授業の重点内容#

Node.js の応用シーン(なぜ)#

  • フロントエンドのエンジニアリング

    • 初期の jQuery などのライブラリは直接ページに読み込まれていましたが、後にモジュール化が成熟し、Node.js は開発者にブラウザ外でコードを実行する能力を与え、フロントエンドは徐々にモジュール化されました。

    • バンドル:webpackViteesbuildParcelなど

    • 圧縮:UglifyJS

    • トランスパイル:babeljsTypeScript

      個人的理解:トランスパイルとは、ES6 のような最新の構文を低バージョンの書き方に変換し、ブラウザの互換性を実現することです。

    • 他の言語がフロントエンドエンジニアリングの競争に参加:esbuildParcel、prisma など

    • 現状:Node.js は置き換えが難しい

  • Web サーバーアプリケーション

    • 学習曲線が緩やかで、開発効率が高い
    • 実行効率は一般的なコンパイル言語に近い
    • コミュニティエコシステムが豊富で、ツールチェーンが成熟している(npm、V8 インスペクター)
    • フロントエンドとの統合シーンにおいて優位性がある(SSR、同構フロントエンドアプリケーション。ページの作成とバックエンドデータの取得と填充はすべて JavaScript で行われます)
    • 現状:競争が激しく、Node.js には独自の優位性がある
  • Electronクロスプラットフォームデスクトップアプリケーション

    • 商業アプリケーション:vscode、slack、discord、zoom
    • 大企業内の効率ツール
    • 現状:ほとんどのシーンで選定時に考慮する価値がある

Node.js ランタイム構造(何)#

image.png

  • N-API:ユーザーコードで npm を使ってインストールしたいくつかのパッケージ
  • V8:JavaScript ランタイム、診断デバッグツール(インスペクター)
  • libuv:eventloop(イベントループ)、syscall(システムコール)
    • 例:node-fetch を使ってリクエストを発行する時
    • このプロセス全体で、底層では非常に多くの C++ コードが呼び出されます

特徴

  • 非同期 I/O:

    setTimeout(() => {
        console.log('B');
    })
    console.log('A');
    
    • 一般的なシーン:ファイルを読み取る時。Node.js が I/O 操作を実行する際、応答が返された後に操作を再開し、スレッドをブロックせず、追加のメモリを待機することはありません。(メモリ使用量が少ない)

      image.png

  • シングルスレッド

    • worker_thread を使って独立したスレッドを起動できますが、各スレッドのモデルには大きな変化はありません

      function fibonacci(num:number):number {
      	if(num === 1 || num === 2) {
              return 1;
          }
          return fibonacci(num-1) + fibonacci(num-2);
      }
      fibonacci(42)
      fibonacci(43)
      
    • JS はシングルスレッド

      • 実際:JS スレッド + uv スレッドプール(4 つのスレッド) + V8 タスクスレッドプール + V8 インスペクタースレッド
    • 利点:マルチスレッドの状態同期問題を考慮する必要がなく、ロックも必要ありません。同時に、システムリソースを比較的効率的に利用できます;

    • 欠点:ブロッキングはより多くの悪影響を及ぼす、非同期問題、遅延に要件があるシーンを考慮する必要があります。

      • 解決策:マルチプロセスまたはマルチスレッド
  • クロスプラットフォーム(ほとんどの機能、API)

    • Linux 上のソケットを使用したいが、異なるプラットフォームでの呼び出しが異なる場合、次のようにするだけです:

      const net = require('net')
      const socket = new net.Socket('/tmp/socket.sock')
      
    • Node.js はクロスプラットフォームであり、JS はコンパイル環境を必要としません(+ Web はクロスプラットフォーム + 診断ツールはクロスプラットフォーム)

      • = 開発コストが低い(ほとんどのシーンでクロスプラットフォームの問題を心配する必要がなく、全体的な学習コストが低い)

Http Server の作成(方法)#

Node.js のインストール#

  • Mac、Linux では nvm の使用を推奨します。複数バージョン管理。
  • Windows では nvm4w または公式インストーラーを推奨します。
  • インストールが遅い、インストールに失敗した場合は、インストールソースを設定します

Http Server + Client の作成、GET、POST リクエストの送受信#

  • 前提:Node.js をインストールし、管理者権限で cmd を開き、現在のファイルディレクトリに移動します

Http Server#

  • まず server.js を作成します。以下のように

    • createServer の説明

      req リクエスト、res レスポンス

      server.listen の説明

      ポートはリッスンするポート番号で、成功後のコールバック関数です

    • const http = require('http');
      const server = http.createServer((req, res) => {
          res.end('hello'); // レスポンスは直接hello
      });
      const port = 3000;
      server.listen(port, () => {
          console.log(`server listens on:${port}`);	// 3000ポートをリッスン
      })
      
      
  • node を使って起動し、この時 localhost:3000 を入力すると hello が表示されます

image.png

image.png

  • JSON 版に変更
  const server = http.createServer((req, res) => {
      // クライアントからのボディを受信
      const bufs = [];    // 受け取ったデータ
      req.on('data', data => {
          bufs.push(data);
      });
      req.on('end', () => {
          const buf = Buffer.concat(bufs).toString('utf-8');
          let msg = 'Hello';
          try {
              reqData = JSON.parse(buf);
              msg = reqData.msg;
          } catch (err) {
              res.end('invalid json');
          }
          // レスポンス
          const responseJson = {
              msg: `receive:${msg}`
          }
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify(responseJson));
      });
  });
  • image.png
  • image.png

Http Client#

const http = require('http');
const body = JSON.stringify({ msg: 'hello from my own client' });
// [url] [option] [callback]
const req = http.request('http://127.0.0.1:3000', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Content-Length': body.length,
    },
}, (res) => {   // レスポンスボディ   
    const bufs = [];
    res.on('data', data => {
        bufs.push(data);
    });
    res.on('end', () => {
        const buf = Buffer.concat(bufs);
        const receive = JSON.parse(buf);
        console.log('receive json.msg is:', receive);
    });
})
req.end(body);

image.png

Promisify#

Promise + async と awaitを使ってこれらの 2 つの例を書き直すことができます(なぜ?)

awaitキーワードが非同期関数と一緒に使われると、その真の利点が明らかになります。実際、await は非同期関数の中でのみ機能します。それは、promise に基づく任意の非同期関数の前に置くことができます。それは、その行でコードを一時停止し、promise が完了するまで待機し、結果値を返します。一時停止中に、他の待機中のコードが実行される機会があります。

async/awaitは、あなたのコードを同期的に見せ、ある程度、それの動作をより同期的にします。awaitキーワードは、promise が完了するまでその後のコードをブロックします。まるで同期操作を実行するかのように。確かに、その間に他のタスクが実行され続けることを許可しますが、あなた自身のコードはブロックされます。

  • コールバックが多すぎると見つけにくく、メンテナンスが難しい

    function wait(t) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve();
            }, t);
        });
    }
    wait(1000).then(() => { console.log('get called'); });
    
  • すべてのコールバックが Promise に書き換えるのに適しているわけではありません

    • 一度だけ呼び出されるコールバック関数に適しています
    const server = http.createServer(async (req, res) => {  // ここでasyncに注意
        // クライアントからのボディを受信することがPromise形式に変更されました
        const msg = await new Promise((resolve, reject) => {    // 実行が完了してからmsgに渡す
            const bufs = [];    
            req.on('data', data => {
                bufs.push(data);
            });
            req.on('error', (err) => {
                reject(err);
            })
            req.on('end', () => {
                const buf = Buffer.concat(bufs).toString('utf-8');
                let msg = 'Hello';
                try {
                    reqData = JSON.parse(buf);
                    msg = reqData.msg;
                } catch (err) {
                    // 
                }
                resolve(msg);
            });
        });
        // レスポンス
        const responseJson = {
            msg: `receive:${msg}`
        }
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify(responseJson));
    });
    

image.png

静的ファイルサーバーの作成#

ユーザーから送信された http リクエストを受け取り、画像の URL を静的ファイルサーバーのディスク上の対応するパスとして取得し、具体的な内容をユーザーに返す簡単な静的サービスを作成します。今回は、httpモジュールに加えて、fsモジュールとpathモジュールも必要です。

まず、static ディレクトリに簡単な index.html を作成します。

image.png

static_server.js#

const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
// __dirnameは現在のファイルの位置、./は現在のファイルのフォルダ、folderPathはstaticフォルダの現在のファイルパスに対する相対パス
const folderPath = path.resolve(__dirname, './static');

const server = http.createServer((req, res) => {  // ここでasyncに注意
    // 期待されるhttp://127.0.0.1:3000/index.html
    const info = url.parse(req.url);

    // static/index.html
    const filepath = path.resolve(folderPath, './'+info.path);
    console.log('filepath', filepath);
    // ストリームスタイルのAPIで、内部メモリ使用率がより良い
    const filestream = fs.createReadStream(filepath);
    filestream.pipe(res);
});
const port = 3000;
server.listen(port, () => {
    console.log(`server listens on:${port}`);
})

image.png

image.png

  • 高性能で信頼性のあるサービスと比較して、何が不足していますか?
  1. CDNキャッシュ + 加速
  2. ** 分散ストレージ、災害復旧(サーバーがダウンしても正常にサービスを提供)

React SSR サービスの作成#

  • SSR(サーバーサイドレンダリング)にはどのような特徴がありますか?
  • 従来の HTML テンプレートエンジンと比較して:コードの重複を避ける
  • SPA(シングルページアプリケーション)と比較して:初回表示が速く、SEO(検索エンジン最適化)に優しい
  • 欠点:
    • 通常 qps(毎秒クエリ率)が低く、フロントエンドコードを書く際にはサーバーサイドレンダリングの状況を考慮する必要があります
    • コードを書くのが難しく、JS を書く際にはフロントエンドの表現も考慮する必要があります

React のインストール#

npm init
npm i react react-dom

サンプルの作成#

const React = require('react');
const ReactDOMServer = require('react-dom/server');
const http = require('http');
function App(props) {
    return React.createElement('div', {}, props.children || 'Hello');
}
const server = http.createServer((req, res) => {
    res.end(`
        <!DOCTYPE html>
        <html>
            <head>
                <title>My Application</title>
            </head>
            <body>
                ${ReactDOMServer.renderToString(
                    React.createElement(App, {}, 'my_content'))}
                <script>
                    alert('yes');
                </script>
            </body>
        </html>
    `);
})
const port = 3000;
server.listen(port, () => {
    console.log('listening on: ', port);
})

image.png

  • SSR の難しさ
  1. コードのバンドルを処理する必要があります
  2. サーバー上でのフロントエンドコードの実行時のロジックを考える必要があります
  3. サーバーに意味のない副作用を取り除くか、環境をリセットする必要があります

インスペクターを使用してデバッグ、診断#

  • V8 インスペクター:開箱即用、特性が豊富で強力、フロントエンド開発と一致、クロスプラットフォーム

    • node -- inspect
    • open http://localhost:9229/json

    image.png

  • シーン:

    • console.log の内容を確認
    • ブレークポイント
    • 高 CPU、無限ループ:cpuprofile
    • 高メモリ使用:heapsnapshot(ヒープスナップショット)
    • パフォーマンス分析

デプロイの概要#

書き終えたら、どうやって本番環境にデプロイしますか?

  • デプロイで解決すべき問題

    • デーモンプログラム:プロセスが終了したときに再起動する

    • マルチプロセス:cluster を使って簡単にマルチプロセスを利用する

    • プロセスの状態を記録し、診断に使用する

  • コンテナ環境

    • 通常、ヘルスチェックの手段があり、マルチコア CPU の利用率を考慮するだけで済みます

延伸トピック#

Node.js コードの迅速な理解#

Node.js Core 貢献入門

  • 利点
    • 使用者の役割から徐々に底層の詳細を理解し、より複雑な問題を解決できるようになります。
    • 自己証明ができ、キャリアの発展に役立ちます。
    • コミュニティの問題を解決し、コミュニティの発展を促進します。
  • 難点:
    • 時間がかかる(実際に)

Node.js のコンパイル#

  • なぜ Node.js のコンパイルを学ぶ必要があるのか
    • 認識:ブラックボックスからホワイトボックスへ、問題が発生したときに手がかりを得ることができます
    • コードを貢献する第一歩
  • コンパイル方法
    • ./configure && make install
    • デモ:net モジュールにカスタム属性を追加

診断 / トレース#

  • 診断は低頻度で重要かつ非常に挑戦的な方向性です。企業が自社の言語に依存できるかどうかを測る重要な参考です。
  • 技術コンサルティング業界での人気のある役割。
  • 難点:
    • Node.js の底層を理解する必要があり、オペレーティングシステムやさまざまなツールを理解する必要があります
    • 経験が必要です

WASM, NAPI#

  • Node.js(V8 のおかげで)は WASM コードを実行するための天然のコンテナであり、ブラウザの WASM と同じランタイムです。また、Node.js は WASI をサポートしています。
  • NAPI は C インターフェースのコード(C/C++/Rust...)を実行し、ネイティブコードのパフォーマンスを保持できます。
  • 異なるプログラミング言語間の通信の一つの解決策です。

まとめと感想#

この授業では Node.js の紹介から始まり、Http Server の実装を行いました(Promise を使ってコールバックを最適化し、SSR についても一定の理解を得ました)。また、延伸トピックでは先生からいくつかの提案や拡張読書もありました、いいですね~

本文引用の内容の大部分は欧陽亚东先生の授業および MDN からのものです。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。