RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

Node.jsの勉強会でお手軽にWebアプリを作った話

はじめに

こんにちは、rs_tukkiです。最近、様々な勉強会に行くことが多くなりました。
大学時代は講義だけ聞いていればいいやーの精神で、自分から技術を学ぶといったことはしてこなかったのですが、
社会人、特にエンジニアともなると、様々な技術へのアンテナを張ることが意外と重要になってきたりします。
で、それは何も特定の技術に絞る必要はなくて、新しい言語のことでも、今話題のスマートスピーカーのことでも、
なんだったらバーチャルYouTuberのことでも大丈夫です。*1その技術を知っているということが、いつか会社内でも大きなアドバンテージになるかもしれません。

さて、今回はそんな勉強会の中から先日参加させていただいたユメノソラホールディングス株式会社様主催のNode.js勉強会で、お手軽にWebアプリケーションを作ったお話をしたいと思います。

yumenosora.connpass.com

大丈夫です。許可は頂きました。

Node.jsとは?

まず作成したものの紹介をする前に、Node.jsという言語の話をしたいと思います。
Node.jsというのは一言でいえば、

サーバサイドで動作できるJavaScript環境のことです。

特徴

サーバサイドで動作できるJavaScript

さて、ここまでの三行で既に、「?」が浮かんでいる人もいるのではないでしょうか。
JavaScriptという言語は本来、ページに動きを与えることをメインとした言語です。Webページに対して、

  • 時計を表示させてみたり
  • 文字をループさせてみたり

等の動作を、いちいちサーバと通信しなくても可能にします。つまり、クライアント側で処理を行うことがほとんどなのです。
ですが、あくまでここまでの仕事は「メイン」です。Node.jsを利用すれば、サーバ側で行っているごにょごにょをJavaScriptで記述して

Node.js「クライアントもサーバも、だいたい全部私におまかせっ!」

な状態にできるのです。全ての処理をほぼNode.js一つで。おお、なんと頼もしい。

シングルスレッド

シングルスレッドとは、
Node.js「同時に一つのことしかできないんだ。ごめんね」
な仕組みのことです。
Apacheなどのウェブサーバでは、マルチスレッド(=同時にいろいろすること)によって沢山の処理をしていますが、それゆえに同時に大量のアクセスがあると、同時にすることが増えすぎて
リソースがなくなってしまう、ということが起こります。この臨界点がだいたい10,000件以上の同時接続にあることから、「C10K(クライアント1万台)問題」と
呼ばれていましたが、Node.jsはこれを解決することができるのです。

ノンブロッキングI/O

さて、Node.jsがシングルスレッドであることは説明しましたが、「じゃあNode.jsは一度にできる処理が少ないの?」というとそうでもありません。
そこで使われるのがノンブロッキングI/Oという仕組みです。これは言ってしまえば
Node.js「こっちの入力、結果出るのに時間かかりそうだから待っててね。代わりにこっちの処理先にやっちゃうよ」
という仕組みのことです。
普通のシングルスレッドの場合は、どんなに処理が遅かろうが出力が出てこなかろうが、ひとつの処理が終わらなければ次の処理に入れませんでした。
この「待ち時間」が多数の処理ができない原因なのですが、ノンブロッキングI/Oだと待ち時間の間に次に来た処理をやっておくので、
一度にたくさんのことをやっ(ているように見せかけ)たりしながら、かつ処理のためのメモリは少なくて済むのです。

メリット

フロント/サーバどちらも一貫してJavaScriptで書ける

先程も説明しましたが、これがNode.jsの最大の魅力だと思います。
簡単なWebアプリケーションをローカルで動かすだけならWebサーバすら不要で、全部Node.jsがやってくれます。

ライブラリが豊富

Node.js、本当にライブラリが多いです。適当におすすめを検索してみるだけでも出てくる出てくる。やってみたいことはだいたいできるんじゃないかってくらいの勢いです。*2
qiita.com
http://cabbalog.blogspot.jp/2017/12/npm-package-25.htmlcabbalog.blogspot.jp

npmってなんぞや?ってなるかと思いますが、
これはNode.jsのライブラリやパッケージなどを管理してくれるツールのことで、Node.jsのインストールにくっついてきます。詳しくはのちほど。

小さな処理を素早く行うのが得意

Node.jsは総じて、小さな処理を次から次へと素早く行うのに適しています。
チャットアプリなど、発言を読み込んで出力する、といった処理なら大得意と言っていいでしょう。

デメリット

同期処理が苦手

できない...とは言わないのですが、Node.js、というかJavascript自体が基本的に非同期処理で動いているので、
同期処理を実現しようとすると結構大変だったりします。このあたりは仕方ないのかも。

重い処理をすると全体のパフォーマンスが低下する

さきほど説明したように、Node.jsはノンブロッキングI/Oなどの仕組みによって少ないメモリで小さな処理を
数多くこなすことができるのですが、その処理自体が大きいと、数をこなすどころの話ではなくなってしまいます。
そのため、なるべく重い処理が関わるアプリには使わないほうが無難でしょう。

Webアプリケーションを作ってみる

さて、それでは早速、Node.jsを使って簡単なWebアプリケーションを作成してみたいと思います。
今回の勉強会で作成したのは、「今(2018年2月)現在放送しているアニメの一覧を表示する」アプリケーションです。

環境構築

Node.js

まずはNode.jsのインストールから。

今回の勉強会では、v8.9.4を利用しました。
一般にNode.jsは、「偶数verが長期サポート型、奇数verが最新機能型」らしいです。推奨は前者なのでこちらをインストール。

インストーラの起動後は画面の指示に従うだけです。インストールするコンポーネントの選択などがありますが基本はデフォルトでOK。 インストールが成功していれば、コマンドプロンプト(orターミナル)を開いて、

node -v 

と打って実行したときに、

v8.9.4

と出てくるはずです。

Atom

続いてエディターですが、特に指定はありません。
今回は使いやすさからAtomをインストールしましたが、EclipseなどのIDEを使う人もいるとか。

npm

さて、Node.jsをインストールした際に、もう一つおまけでインストールされているものがあります。
それが先ほど説明したnpm。Node.jsのライブラリやパッケージなんかを管理してくれるすごいやつです。

ひとまずは初期化処理から。 npmがインストールしたものを管理する、package.jsonというファイルを作成します。
インストール用のフォルダを作成して、その中で

npm init

を実行してください。すると、

Press ^C at any time to quit.
package name: (test)  //パッケージ名
version: (1.0.0)  //バージョン名
description:  //概要説明
entry point: (index.js)  //初期表示させるファイル名
test command:  //テストコマンド
git repository:  //Githubに保存するリポジトリ情報
keywords:  //npmの公開時に使用されるキーワード
author:  //作者情報
license: (ISC)  //npmの公開時に適用する権利情報

とまあやたら入力を求められます。とはいえ、パッケージの公開をしないのならこの辺りは全部無視でもOKです。
これが完了すると、初期化を行ったフォルダ内に、フォルダとファイルが1つづつ作成されています。

package.jsonにはインストールしていったものの情報が蓄積されていきます。そして、その実体はnode_moduleフォルダに格納されていきます。
今後、package.jsonがあるこのフォルダ内で色々インストールしていくことになります。

EJS

では早速、npmでひとつインストールしてみましょう。
今回は、Node.jsを使ったサイトのコーディングを、jspのような感じにできるEJSというテンプレートエンジンをインストールします。
テンプレートエンジンも他に色々ありますので、よければ探してみてください。

さて、先ほどのpackage.jsonがあるフォルダで

npm install ejs

と打ち込みます。package.jsonの設定を無視したので何やら警告は出ます*3が、大体数秒でインストールは完了します。
これでもう今回のコードを書く準備が完了しました。さすが環境設定も早い。

コードを書いてみる

ではここからやっとこさコードを書く作業に移ります。
勉強会では何回かに分けて順番にコードを書いたのですが...せっかくなので、完成版を見て解説していきましょう。
まずは肝となるjsファイルから。

practice2.js

//モジュールを拡張機能として読み込む
var http = require('http');
var fs = require('fs');
var ejs = require('ejs');
var url = 'http://api.moemoe.tokyo/anime/v1/master/2018/1?ogp=1';//取得するjsonファイル

var hostname = '127.0.0.1';
var port = 3000;
var server = http.createServer();//httpのサーバを作成するぞー、という関数

server.on('request', function(req, res) {//httpリクエストがあった(=アクセスされた)時に呼ばれる  

    http.get(url, function(apiRes) {//指定したURLが取得出来たら呼ばれる

        var body = '';
        apiRes.setEncoding('utf8');
    
        apiRes.on('data', function(chunk) {//データが受信されたら呼ばれる
            body += chunk;
        });

        apiRes.on('end', function() {//データの受信が終わったら呼ばれる
            var data = {};
            data.animes = JSON.parse(body);
            var template = fs.readFileSync('./practice2.ejs', 'utf-8');
            var page = ejs.render(template, data);
            res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
            res.write(page);
            res.end();
        });
    });
});

server.listen(port, hostname, function() {//サーバ起動時に呼ばれる
    console.log(`Server runnning at http://${hostname}:${port}/`);
});

色々と関数がありますが、だいたいは~したときに呼ばれるという形になっています。これはイベントループという仕組みが使われているためで、平たく言えば

Node.js「とりあえず待ってるから、来たものから先にやるからね!」

という処理です。今回の場合は、最初に呼ばれるのは一番下のserver.listenになるでしょう。
...私の解説は上から行きます。

//モジュールを拡張機能として読み込む
var http = require('http');
var fs = require('fs');
var ejs = require('ejs');
var url = 'http://api.moemoe.tokyo/anime/v1/master/2018/1?ogp=1';//取得するjsonファイル

var hostname = '127.0.0.1';
var port = 3000;
var server = http.createServer();//httpのサーバを作成するぞー、という関数

まずは必要な変数の指定です。
requierというのは、Node.jsのモジュールを拡張機能として読み込む処理です。先ほどインストールしたejsの姿も。
httpは文字通りHTTPの各種機能をまとめたもので、fsはファイル操作関連の機能が入っています。

urlには、表示させるアニメ情報が書かれたテキストファイル*4が入っています。このデータを取得してejsで表示させるのが今回の目標。

hostnameportは文字通りですね。アクセスに必要です。127.0.0.1というのはいわばlocalhostのことです。

そしてserver。この処理だけでhttpサーバは作成できます。簡単すぎる...
この引数に、作成時の処理を書くのが一般的らしいですが、今回は分かりにくいのでその処理は後で。

server.on('request', function(req, res) {//httpリクエストがあった(=アクセスされた)時に呼ばれる  

    http.get(url, function(apiRes) {//指定したURLが取得出来たら呼ばれる

        var body = '';
        apiRes.setEncoding('utf8');

サーバはずっと待機状態で、指定されたポートにアクセスがあった場合に「待ってました!」と言ってserver.on以降の処理を始めます。
http.getは、引数に指定されたurlの中身を取得しようとします。成功すると次へ。
今回取得したファイルの中身はテキストファイルなので、まずは格納用の変数の用意と、文字コードのセットを行いましょう。

        apiRes.on('data', function(chunk) {//データが受信されたら呼ばれる
            body += chunk;
        });

        apiRes.on('end', function() {//データの受信が終わったら呼ばれる
            var data = {};
            data.animes = JSON.parse(body);
            var template = fs.readFileSync('./practice2.ejs', 'utf-8');
            var page = ejs.render(template, data);
            res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
            res.write(page);
            res.end();
        });
    });
});

では、ここからファイルの中身を取得していきます。
「受信したら~」と「受信が終わったら~」に分かれているのは、データがあまりにも多すぎると
Node.jsは取得の完了を待たずに次の処理を始めることができるためです。
いちいち全部取得してからやるより、データを取得するたびにbodyの中に次から次へと追加していったほうが処理が早いのです。
で、無事受信が完了したら、取得したテキストファイルはjson形式に変えられて、dataに格納されます。

さて、次に行う処理ですが、ここでは表示するページの設定を行っています。
ejsファイルを読み込み、ejs.renderで先ほど取得したデータをejsページに送信しています。
res.writeHeadでHTTPヘッダを記述し、res.writeでページを指定してやればページが表示できます。

最後に、res.endで読み込みを完了させてやれば終了です。

server.listen(port, hostname, function() {//サーバ起動時に呼ばれる
    console.log(`Server runnning at http://${hostname}:${port}/`);
});

そして、これが本来最初に行われる処理です。サーバ起動時にどのような動作をするか、を記述します。
今回はコンソール出力をしておきましょう。

では、続いて表示側。

practice2.ejs

<html>
  <head>
    <title>ejs_sample</title>
    <style type="text/css">
      .div_block { width: 300px; display: inline-block; vertical-align: top;
                   margin: 5px; border: 1px solid #555; }
      .div_header { background-color: #CCC; padding: 10px;
                    height: 26px; font-size: 12px;}
      img { object-fit: cover; width: 300px; height: 160px; display:block;}
      .nodata { width: 300px; height: 160px; background-color: #EEE;}
      p { padding: 5px; font-size: 10px; display: block; height: 60px;}
    </style>
  </head>
  <body>

    <%# ヘッダー %>
    <h1>今期のアニメ</h1>
    <%# アニメ数分ループ %>
    <% for (anime of animes) { %>
      <div class="div_block">
        <div class="div_header">
          <a href="<%= anime.public_url %>"><%= anime.title %></a>
        </div>
        <% if (!!anime.ogp.og_image) { %>
          <img src="<%= anime.ogp.og_image %>" />
        <% } else { %>
          <div class="nodata">no data</div>
        <% } %>  
        <p>
          <% if (!!anime.ogp.og_description) { %>
            <%= anime.ogp.og_description %>
          <% } else { %>
            no data
          <% } %>
        </p>
      </div>
    <% } %>
  </body>
</html>

jspライクに記述ができるので、htmlに見えるけどfor文やif文が入ってたりします。
では上から。

<html>
  <head>
    <title>ejs_sample</title>
    <style type="text/css">
      .div_block { width: 300px; display: inline-block; vertical-align: top;
                   margin: 5px; border: 1px solid #555; }
      .div_header { background-color: #CCC; padding: 10px;
                    height: 26px; font-size: 12px;}
      img { object-fit: cover; width: 300px; height: 160px; display:block;}
      .nodata { width: 300px; height: 160px; background-color: #EEE;}
      p { padding: 5px; font-size: 10px; display: block; height: 60px;}
    </style>
  </head>

この部分は丸々cssが記述されています。詳しくは見ませんが、アニメ情報を一つづつ並べるためのstyleを設定しています。

    <%# ヘッダー %>
    <h1>今期のアニメ</h1>
    <%# アニメ数分ループ %>
    <% for (anime of animes) { %>
      <div class="div_block">
        <div class="div_header">
          <a href="<%= anime.public_url %>"><%= anime.title %></a>
        </div>

for文が出てきました。ひとまず、EJS独自の構文について説明します。
<%# %>はコメント扱いです。出力結果には影響しません。
<% %>は、内部がそのままjavascript構文になります。for文やif文を使いたい場所に仕込めば、簡単にループや分岐が実現できます。
<%= %>は、javascript内の変数の出力です。今回はjsonの中身を指定して出力しています。

        <% if (!!anime.ogp.og_image) { %>
          <img src="<%= anime.ogp.og_image %>" />
        <% } else { %>
          <div class="nodata">no data</div>
        <% } %>  
        <p>
          <% if (!!anime.ogp.og_description) { %>
            <%= anime.ogp.og_description %>
          <% } else { %>
            no data
          <% } %>
        </p>
      </div>
    <% } %>
  </body>
</html>

今度はif文です。取得したjsonファイルには、画像や説明の中身がないものもあるので、
そういった箇所にはとりあえずno dataと書いておきましょう。

...あ、for文やif文の閉じタグも<% %>で囲うのを忘れずに。

実行!

はい、これでひとまず完成です!すごい!かんたん!

というわけで、とりあえず実行してみます。コマンドプロンプト(or ターミナル) を開いて、

node practice2.js

...これだけ。上手く起動できれば、

Server running at http://127.0.0.1:3000

と出てきますので、早速このアドレスにアクセスしてみましょう。

しっかり画像付きで、アニメの一覧が表示されました!(流石にここに表示するわけにはいかないので隠してますが...)

おわりに

Node.jsがサーバサイドでしっかり動作してるのを確認いただけたでしょうか。
今回は簡単なアプリの作成のみでしたが、実際はもっとずっと色々なことが出来たりします。
言語を色々覚えなくてもサーバサイドまで処理できるので、初心者向けではないでしょうか。ぜひともみんなでやってみましょう!

あ、それと、勉強会、楽しいからみんな参加しよう!

おまけ

就活、いよいよ始まりましたね!
私としては、この時期だからこそ合同説明会に参加すべきだと考えています。
自分の働きたい職種が決まっていればそれらを全部見て回ると比較ができますし、そうでなくても
気になった会社の話を聴いてみると、就活のイメージがつかめると思います!

ぜひとも就活、頑張ってください!そしてご縁がありましたら、ラクスでお待ちしています!
fresh-recruit.rakus.co.jp

参考


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

*1:この辺り、私が実際に参加した/参加を予定している勉強会の実例です

*2:その数、2017年11月現在で475,000件!

*3:descriptionがないとか、repositoryがないとかいった感じです。package.json自体がなくても警告が出ます

*4:のちほどjsonに変換するため、json形式で書かれています。

Copyright © RAKUS Co., Ltd. All rights reserved.