サーバーサイド

【Node.js】 静的・動的なWebサーバーを構築してみる

更新日:2020/05/05

前回はNode.jsをWindowsにインストールしたので、今回はWebサーバー化してみます。

 

Webサーバーを構築基礎

Webサーバーを構築といっても、Node.js自体にサーバー機能はありません。
Node.jsを使って、自分でWebサーバーを作るという方が正しいです。

次のJavaScriptは、httpプロトコルでポート3000にアクセスされたとき、"Node.js Web Server"と返しています。


const http = require("http");
const server = http.createServer();

server.on("request", function( request, response ) {
    response.writeHead(200, {"Content-Type" : "text/plain"});
    response.write( "Node.js Web Server" );
    response.end();
});

server.listen(3000);

※やっていること

  1. Node.jsにはHTTP通信を処理してくれるモジュールが標準であるので、最初に読み込んでいます。
  2. server.onに、ポートにアクセスされたときに実行する関数を登録します。
  3. 最後にserver.listenで待ち受けポートを登録しています。

コマンドプロンプトで実行してみます。

c:\Users\kchan > node 作成したファイルの名前

このスクリプトは終了しないで、待ち受け状態になります。
終了したいときは、ctrl+cを入力します。

待ち受け状態のまま、ブラウザで、http://localhost:3000/にアクセスします。

Node.js Webサーバー 実行結果

表示されました。

index.htmlを表示したいときは、次のコードのようにhtmlファイルを読み込んで表示します。


const http = require("http");
const server = http.createServer();
const fs = require("fs");

server.on("request", function( request , response ) {
    fs.readFile("index.html", "utf-8",
        ( error , content ) => {
               response.writeHead(200, { "Content-Type": "text/html" });
               response.write( content );
                response.end();
        });
});
server.listen(3000);

エラー処理をしていないので、運用する場合はもっと複雑になりますね。

 

手軽にWebサーバーを実装

上の例は、ポートへのアクセスに対して単一の結果を返すだけなので、Webサーバーとは言い難いですね。
汎用性を高めたいなら、リクエストからパスを解析して対応するファイルとファイルタイプを返したりする必要があります。

いろいろ面倒ですね。
静的なファイルを返すだけならhttp-serverというモジュールが楽です。

  1. http-serverのインストール

    npmコマンドを使用して、http-serverをインストールします。

    コマンド:npm install -g http-server

    c:\Users\kchan > npm install -g http-server
    C:\Users\kchan\AppData\Roaming\npm\http-server -> C:\Users\kchan\AppData\Roaming\npm\node_modules\http-server\bin\http-server
    C:\Users\kchan\AppData\Roaming\npm\hs -> C:\Users\kchan\AppData\Roaming\npm\node_modules\http-server\bin\http-server
    + http-server@0.12.3
    added 23 packages from 35 contributors in 2.568s

    -g を使用しないと、現在のフォルダに。
    使用すると、npmの規定のフォルダにインストールされます。

  2. サイトの準備

    適当なフォルダに、適当なhtmlファイル作成します。

    webroot
        ├ index.html
        └ folder1
                └  index2.html

  3. http-serverの起動

    作成したフォルダに移動して、http-serverを起動します。

    コマンド:http-server [-p ポート]

    -pは省略可能で、省略すると8080になります。

    c:\Users\kchan > http-server
    Starting up http-server, serving ./
    Available on:
    http://127.0.0.1:8080
    Hit CTRL-C to stop the server

  4. 確認

    ブラウザで、フォルダ内のhtmlにアクセスしてみて、表示されたら成功です。

静的ページのみのWebサーバーが、簡単にできました。

 

動的なHtml生成を行う

静的なファイルを返しつつ、htmlのみPHPみたいに動的に生成してみます。

今回は expressというフレームワークを使用します。

その前に、今回はサーバー側のプログラムとWebサイトを分離したかったので、次のようなフォルダ(ディレクトリ)構造にしてみました。

フォルダ構造

web
    ├ server // Webサーバースクリプト
    └ webroot // webサイトのルート

expressをインストール

serverフォルダに移動して、 expressをインストールします。

  1. cd web\server

    serverフォルダに移動します

    c:\Users\kchan > cd web\server

  2. npm init -y

    インストールに必要なpackage.jsonを、作成します。

    C:\Users\kchan\nodejsweb\server>npm init -y
    Wrote to C:\Users\kchan\web\server\package.json:
    {
      "name": "server",
      "version": "1.0.0",
      "description": "",
      "main": "server.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }

  3. npm install express

    expressをインストールします。
    -gでインストールすると動作しなかったので、カレントフォルダにインストールします。

    C:\Users\kchan\web\server>npm install express
    npm notice created a lockfile as package-lock.json. You should commit this file.
    npm WARN server@1.0.0 No description
    npm WARN server@1.0.0 No repository field.

    + express@4.17.1
    added 50 packages from 37 contributors and audited 126 packages in 2.438s
    found 0 vulnerabilities

    warningが出ています…とりあえず無視で…

  4. npm install ejs

    テンプレート化したhtmlを処理するために、ejsをインストールします。
    こちらも-gでインストールすると動作しなかったので、カレントフォルダにインストールします。

    C:\Users\kchan\web\server>npm install ejs

    > ejs@3.1.2 postinstall C:\Users\kchan\web\server\node_modules\ejs
    > node --harmony ./postinstall.js

    Thank you for installing EJS: built with the Jake JavaScript build tool (https://jakejs.com/)

    npm WARN server@1.0.0 No description
    npm WARN server@1.0.0 No repository field.

    + ejs@3.1.2
    added 15 packages from 8 contributors and audited 145 packages in 1.715s
    found 0 vulnerabilities

  5. npm install body-parser

    body-parserは、expressでpostデータを受け取るのに必要。

    C:\Users\kchan\web\server>npm install body-parser
    npm WARN server@1.0.0 No description
    npm WARN server@1.0.0 No repository field.

    + body-parser@1.19.0
    updated 1 package and audited 177 packages in 1.454s
    found 0 vulnerabilities

    expressでのPOSTデータ取得方法は、バージョンによって異なるらしい。
    現時点(2020.5)ではbody-parserだが、今後変わるかもしれない。

※npm install は全部一度に指定してもいいようです・・・

サーバー用のスクリプトを作成

次のような条件で、Webサーバーを構築してみます。

  • メソッドがgetで拡張子が .js、.css、.jpg、.png のとき、ファイル内容をそのまま送信。
  • メソッドがget・postで拡張子が .html で ファイル名.html.ejs があるとき、ファイル名.html.ejsをejsでフォーマットして送信。

    なければ、htmlをそのまま送信。

  • メソッドがget・postでurlが/のとき、index.htmlを送信。ejsがあればフォーマットする。

スクリプトファイルは二つに分けてあります。
server.js    … expressの設定やイベント登録など
serverlib.js … イベント処理

server.js


const serverlib = require("./serverlib.js");
const WebRoot = "../webroot";
serverlib.WebRoot = WebRoot;
serverlib.RenderExt = ".ejs";

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

        // ejsでレンダーさせる設定
app.set("view engine", "ejs");

        // expressでpostデータを受け取る3行のおまじない
const bodyparser = require("body-parser")
app.use(bodyparser.urlencoded({extended: true}))
app.use(bodyparser.json())

app.get( serverlib.getReg , ( request, response) => serverlib.SendFile(request, response) );
app.post( serverlib.postReg , ( request, response) => serverlib.SendFile(request, response) );

app.listen(8080);

■やっていること

app.getでgetを、app.postでクライアントからのpost要求を拾っています。

serverlib.getRegは、/(\.js|\.css|\.jpg|\.png|\.html|\/)$/。
serverlib.postRegは /(\.html|\/)$/ がセットされていて、urlをフィルタしています。

どちらの正規表現も、 .(ドット)を含んだ拡張子がキャプチャされます。

serverlib.js


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

(()=>{

   let WebRoot="";
   let RenderExt="";

   const funcs = {
           indexServe : ( request ,response ) =>  // /でアクセスした場合index.htmlを表示
                render(request ,response ,"index.html"),

           defaultServe : ( request ,response ) => // ファイルをそのまま表示
                    FileSend( fileExist(request) , response ),

        renderHtml : ( request ,response ) =>  // .html
                    render(request ,response) ,
    };
            // 許可する静的ファイルの拡張子
    const AllowExtents = [".js",".css",".jpg",".png"]; 
    
            // 動的な処理をするファイルの拡張子と処理内容
    const fileExtentsFunc = {  
        "/": funcs.indexServe,
        ".html" : funcs.renderHtml,
    };
             // パスをテストするための正規表現を作成 post用
    const postReg = createReg(fileExtentsFunc);
    
           // fileExtentsFuncにAllowExtentsを統合
    AllowExtents.forEach( e => fileExtentsFunc[e] = funcs.defaultServe );
    
           // パスをテストするための正規表現を作成 get用
    const getReg = createReg(fileExtentsFunc);
    
    function createReg( obj ){
        return new RegExp("(" + 
                     Object.keys(obj)         // キーを配列に変換
                         .sort( (a,b) =>b.length - a.length ) // キーを文字数が多い順にソート
                         .map( e => e.replace(/\./g,"\\.") )  // . を \\.に置換
                         .join("|") +        // | を区切りとして一つの文字列へ
                    ")$");
    }
    
    const FileSendeOpt ={
        dotfiles : "deny"
    };

        // 静的ファイルの送信
    function FileSend( filename , response ){
        
            if( filename === null ) resError( response , 404 );
            else{
                response.sendFile( path.join( __dirname , filename ) , FileSendeOpt, 
                        function (err) {
                            if( err ) resError( response , 404 );
                        });
            }
    }
        // テンプレートファイルがあればレンダーして送信
        // なければ静的ファイルを送信
    function render( request , response , addName = ""){
            const fpath = fileExist( request , addName + RenderExt );
    
            if( fpath !== null ) response.render(path.join( __dirname,fpath)
                                , { get : request.query , post : request.body } );
            else FileSend( fileExist( request , addName ) , response );
    }
          // ファイルがない=null ある=パス を返す
    function fileExist( request , addExt = ""){
            const path = url.parse( request.url , true).pathname + addExt;
            if ( path.includes( "/.." ) ) return null;
    
            const rpath =  WebRoot + path;

            try{
                fs.statSync(rpath);
                return rpath;
            }catch(error){

                return null;
            }
    }

    function resError( response , type ){
        response.sendStatus(type);
    }

        // exportsにプロパティ登録・外部に公開
    Object.defineProperties(exports, {
                 getReg :{
                        value :  getReg,
                        enumerable : true,
                    },
                postReg :{
                        value :  postReg,
                        enumerable : true,
                    },
                 SendFile :{
                        value :  ( request ,response ) => fileExtentsFunc[request.params[0]]( request ,response ),
                        enumerable : true,
                    },
                WebRoot: {
                           set : ( r ) => WebRoot = r,
                           enumerable : true,
                    },
                RenderExt: {
                           set : ( r ) => RenderExt = r,
                           enumerable : true,
                    },
            });
    Object.freeze(exports);

})();

■やっていること

  • get/post用正規表現作成。
  • 拡張子ごとの実行関数定義。

    const fileExtentsFunc = {
    "/": funcs.indexServe,
    ".html" : funcs.renderHtml,
    ".js" : funcs.defaultServe,
    ".css" : .funcs.defaultServe,
    ・・・・
    };

    .js以降は、AllowExtentsの内容を元に作成。

  • Object.definePropertiesで、exportsにプロパティ登録。

     

    まとめ

    僕の今までの経験上、Webサーバーといえばapacheやnginxだったので、不特定のパス階層にあるスクリプト(PHP)を実行できるのが当たり前でした。

    しかしネットで、Node.jsの情報を調べても不特定のパス階層にアクセスする例がほとんどなかった。

    あったとしても静的なファイルをそのまま返すだけで、拡張子ごとに処理を変更するものがない。

    まあApacheなどは多数の環境で動作している実績がある。
    それに対して、Node.jsは個々で作成したスクリプトのため、信用性が低い。

    ぶっちゃけ、不特定多数なんてセキュリティ的な問題を組み込んでいるようなもんだ。

    その意味では、この記事はNode.jsの考え方にそぐわないのかもしれない。

    とりあえず想定していた動きができたので、僕としては満足です。

    更新日:2020/05/05

    書いた人(管理人):けーちゃん

    スポンサーリンク

    記事の内容について

    null

    こんにちはけーちゃんです。
    説明するのって難しいですね。

    「なんか言ってることおかしくない?」
    たぶん、こんなご意見あると思います。

    裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
    そんなときは、ご意見もらえたら嬉しいです。

    掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
    生暖かい視線でスルーするか、ご指摘ください。

    ご意見、ご指摘はこちら。
    https://note.affi-sapo-sv.com/info.php

     

    このサイトは、リンクフリーです。大歓迎です。