20191202のNode.jsに関する記事は12件です。

[exercism.io]VSCodeでJestのデバッグをする方法

概要

exercism.ioのJavaScriptコースでJestのデバッグをする方法を紹介します。
といっても結局はVSCodeでJestのデバッグをする方法です。

方法

1.launch.jsonに以下の記述をする。
programのパスはjestの実行ファイルがある場所に変えてください。


{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Jest Debugger",
            "program": "${workspaceFolder}/javascript/list-ops/node_modules/.bin/jest",
            "args": [ "${fileBasenameNoExtension}" ],
            "cwd": "${workspaceFolder}/${relativeFileDirname}",
        }
    ]
}

2.デバッグしたいファイルを開いた状態でDebugを実行する

how to debug.png

解説

気が向いたときに補足します。

programがデバッグ実行時に呼ばれる処理です。
argsが処理が呼ばれる際に渡される引数です。

なのでデバッグ実行時にはjestに現在開いているファイル名を渡しています。

とりあえず雑に参考資料を貼っておきます。
https://code.visualstudio.com/docs/editor/variables-reference
https://code.visualstudio.com/Docs/editor/debugging

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NestJS の Module と DI を理解する

この記事は NestJS アドベントカレンダー 2 日目の記事です。

はじめに

昨日の記事ではアプリケーションを作って一通り動かすところまで説明されました。
この中では Module については、デフォルトで生成される AppModule のまま使用しておりますが、大規模になるにつれて Module を分割することになると思います。
この記事では、 Module の概要と、 Module を分割することによる DI への影響を説明します。
公式のドキュメントにも説明がありますので、合わせて読んでいただくことでより理解が深まると思います。

サンプルコードのリポジトリは以下になります。

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day2-understanting-module-and-di

なお、環境は執筆時点での Node.js の LTS である v12.13.1 を前提とします。

NestJS における Module とは

NestJS では任意の controller, provider(service など)をまとめた単位を Module といいます。
TypeScript の class 定義に対して、 @Module() Decorator を使用して定義します。この時、 class 定義は空でも問題ありません。

昨日の例では全て AppModule 内に定義しましたが、 AppController と AppService の実装を ItemsModule に移してみます。この場合、以下のように定義されます。

items/items.module.ts
@Module({
  controllers: [ItemsController],
  providers: [ItemsService],
})
export class ItemsModule {}

また、 AppModule では import に以下のように定義します。

app.module.ts
@Module({
  imports: [ItemsModule],
  controllers: [AppController],
})
export class AppModule {}

上記の controllers, providers, import の他に、Module に定義した provider を他の Module でも使用するための export があります。 export については後述します。

基本的には Module の内部で DI のスコープも完結します。これを試すため、以下で Comments Module を実装します。

cli を用いて CommentsModule を生成する

新たに Module を作成する場合、 @nestjs/cli を使用すると、 AppModule への反映も自動で行ってくれるため便利です。

$ yarn add -D @nestjs/cli
$ yarn nest g module comments

コマンドを実行すると、以下のようにファイル生成と更新が行われていることがわかります。

CREATE /src/comments/comments.module.ts (85 bytes)
UPDATE /src/app.module.ts (324 bytes)

AppModule の import に CommentsModule が追加されていますね。

app.module.ts
@Module({
  imports: [ItemsModule, CommentsModule],
  controllers: [AppController],
})
export class AppModule {}

同様に controller と service も cli を使うことで生成すると共に該当する Module の controllers / providers に自動追記されます。

CommentsModule を実装し、動作を確認する

以下のように CommentsController と CommentsService を実装していきます。

comments/comments.controller.ts
@Controller('comments')
export class CommentsController {
  constructor(private readonly commentsService: CommentsService) {}

  @Get()
  getCommentsByItemId(@Query() query: { itemId: string }): Comment[] {
    return this.commentsService.getCommentsByItemId(+query.itemId);
  }
}
comments/comments.service.ts
export interface Comment {
  id: number;
  itemId: number;
  body: string;
}

const comments: Comment[] = [
  {
    id: 1,
    itemId: 1,
    body: 'Hello, I am Alice',
  },
  {
    id: 2,
    itemId: 1,
    body: 'Hello, I am Beth',
  },
  {
    id: 3,
    itemId: 2,
    body: 'That is also love.',
  },
];

@Injectable()
export class CommentsService {
  getCommentsByItemId(itemId: number): Comment[] {
    return comments.filter(comment => comment.itemId === itemId);
  }
}

curl コマンドで動作確認をします。

$ curl localhost:3000/comments\?itemId=1
[{"id":1,"itemId":1,"body":"Hello, I am Alice"},{"id":2,"itemId":1,"body":"Hello, I am Beth"}]

テストも追加していきます。

comments/comments.controller.spec.ts
describe('Comments Controller', () => {
  let commentsController: CommentsController;
  let commentsService: CommentsService;

  beforeEach(async () => {
    commentsService = new CommentsService();
    commentsController = new CommentsController(commentsService);
  });

  describe('/comments', () => {
    it('should return comments', () => {
      const comments: Comment[] = [
        {
          id: 1,
          itemId: 1,
          body: 'Mock Comment',
        },
      ];
      jest
        .spyOn(commentsService, 'getCommentsByItemId')
        .mockImplementation(() => {
          return comments;
        });
      expect(
        commentsController.getCommentsByItemId({ itemId: '1' }),
      ).toHaveLength(1);
    });
  });
});
comments/comments.service.spec.ts
describe('CommentsService', () => {
  let commentsService: CommentsService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [CommentsService],
    }).compile();

    commentsService = module.get<CommentsService>(CommentsService);
  });

  it('should be defined', () => {
    expect(commentsService).toBeDefined();
  });

  describe('getCommentsByItemId', () => {
    it('should return comments if exist', () => {
      const comments = commentsService.getCommentsByItemId(1);
      expect(comments.length).toBeTruthy();
    });

    it('should return empty array if not exist', () => {
      const comments = commentsService.getCommentsByItemId(0);
      expect(comments).toHaveLength(0);
    });
  });
});

なお、自明である内容をテストしている箇所があるため、今後はテストが必要であるところのみ、テストを記述します。

Module 間で DI のスコープが別れていることを確認する

Module をまたいだ DI は行えないため、 ItemsController で CommentsService を使用することはできません。
ItemsConbtroller に以下を実装し、確認します。

items/items.controller.ts
interface GetItemWithCommentsResponseType {
  item: PublicItem;
  comments: Comment[];
}

@Controller()
export class ItemsController {
  constructor(
    private readonly itemsService: ItemsService,
    private readonly commentsService: CommentsService,
  ) {}

  @Get()
  getItems(): PublicItem[] {
    return this.itemsService.getPublicItems();
  }

  @Get(':id/comments')
  getItemWithComments(@Param()
  param: {
    id: string;
  }): GetItemWithCommentsResponseType {
    const item = this.itemsService.getItemById(+param.id);
    const comments = this.commentsService.getCommentsByItemId(+param.id);

    return { item, comments };
  }
}

この状態で $ yarn start:dev で起動すると、 DI が解決できない旨のエラーが表示されます。

[ExceptionHandler] Nest can't resolve dependencies of the ItemsController (ItemsService, ?). Please make sure that the argument CommentsService at index [1] is available in the ItemsModule context.

Potential solutions:
- If CommentsService is a provider, is it part of the current ItemsModule?
- If CommentsService is exported from a separate @Module, is that module imported within ItemsModule?
  @Module({
    imports: [ /* the Module containing CommentsService */ ]
  })

別の Module の Service を使うために export する

CommentsService は別 Module にあるので、エラーメッセージに沿って以下のように修正します。

  • CommentsModule で CommentsService を export する
  • ItemsModule で CommentsModule を import する
comments/comments.module.ts
@Module({
  controllers: [CommentsController],
  providers: [CommentsService],
  exports: [CommentsService],
})
export class CommentsModule {}
items/items.module.ts
@Module({
  imports: [CommentsModule],
  controllers: [ItemsController],
  providers: [ItemsService],
})
export class ItemsModule {}

ここで重要なのは export が必要ということで、ただ CommentsModule を import するだけでは同様のエラーとなります。
export を使用してはじめて他の Module から参照可能になるということです。

同様にテストを追記し、 curl で動作確認を行います。

items/items.controller.spec.ts
  describe('/items/1/comments', () => {
    it('should return public item and comments', () => {
      const item: PublicItem = {
        id: 1,
        title: 'Mock Title',
        body: 'Mock Body',
      };
      const comments: Comment[] = [
        {
          id: 1,
          itemId: 1,
          body: 'Mock Comment',
        },
      ];
      jest.spyOn(itemsService, 'getItemById').mockImplementation(() => {
        return item;
      });
      jest
        .spyOn(commentsService, 'getCommentsByItemId')
        .mockImplementation(() => {
          return comments;
        });
      expect(itemsController.getItemWithComments({ id: '1' })).toEqual({
        item,
        comments,
      });
    });
  });
$ curl localhost:3000/items/1/comments
{"item":{"id":1,"title":"Item title","body":"Hello, World"},"comments":[{"id":1,"itemId":1,"body":"Hello, I am Alice"},{"id":2,"itemId":1,"body":"Hello, I am Beth"}]}

無事動作しましたので完成です。

おわりに

これで Module の概要と、 DI との関係性は伝えられたかと思います。
しかし今回は Module の基礎に留めたため、まだ紹介しきれていない機能もあるため、公式ドキュメントを読みながら試すのが早いかと思います。
また、今後アドベントカレンダーや Japan NestJS Users Group でテクニックを発信していく予定ですので、併せてご興味を持っていただけますと嬉しいです。

明日は @potato4d さんが DTO と Request Validation についてお話する予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsで作っているサイトにKARTEのタグを入れてみるメモ

Nuxt.jsで作っているWebサイトKARTEを入れてみたときのメモです。

NuxtにGoogle アナリティクスを導入する手順とほぼ同じで出来た

公式のGoogle アナリティクスを使うには?が参考になります。

読んでみると

他のトラッキングサービスでも、同様の方法を使うことができます。

って書いてますね。

1. plugins/karte.jsを作成

こんな雰囲気でpluginsの中にkarte.jsを作成します。

スクリーンショット 2019-12-02 22.53.40.png

KARTEの管理画面で計測タグを探してスクリプトタグの内部を見つけましょう。

plugins/karte.js
export default ({ app }) => {
    /*
    ** クライアントサイドかつプロダクションモードでのみ実行
    */
    if (process.env.NODE_ENV !== 'production') return
    /*
    ** karteのスクリプトをインクルード
    */
    (function(){var t,e,n,r,a;for(t=function(){var t;return t=[],function(){var e,n,r,a;for(n=["init","start","stop","user","track","action","event","goal","chat","buy","page","view","admin","group","alias","ready","link","form","click","submit","cmd","emit","on","send","css","js","style","option","get","set","collection"],e=function(e){return function(){return t.push([e].concat(Array.prototype.slice.call(arguments,0)))}},r=0,a=[];r<n.length;)t[n[r]]=e(n[r]),a.push(r++);return a}(),t.init=function(e,n){var r,a;return t.api_key=e,t.options=n||{},a=document.createElement("script"),a.type="text/javascript",a.async=!0,a.charset="utf-8",a.src=t.options.tracker_url||"https://static.karte.io/libs/tracker.js",r=document.getElementsByTagName("script")[0],r.parentNode.insertBefore(a,r)},t},r=window.karte_tracker_names||["tracker"],e=0,n=r.length;n>e;e++)a=r[e],window[a]||(window[a]=t());tracker.init("xxxxxxxxxxxxxxxxxxxxxxxxxx")}).call(this);
   /* ↑計測タグの中身をコピペ*/ 

}

IDっぽいところをxxxx~~にしてますが、こんな感じで入れ込みます。

ここのタグは人によって権限っぽい指定の箇所が変わるかもしれないので、この記事からよりは、大元からのコピペが良いと思います。

2. nuxt.config.jsの設定

利用するにあたり、nuxt.config.jsにも以下の記載をします。

nuxt.config.js
省略

  plugins: [
    { src: '~plugins/karte.js', mode: 'client' }
  ],

省略

以上!

所感

割と簡単に出来ました。

実際に僕の場合はnuxt generateで静的サイトにしてホスティングしていますが、静的ホスティングじゃない場合でも同様に動くと思います。(たぶん)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】wsl2上に最新安定板のnode.jsをインストールする方法

筆者環境

  • Microsoft Windows [Version 10.0.19033.1]
  • wsl2
    • NAME="Ubuntu"
    • VERSION="18.04.3 LTS (Bionic Beaver)"

aptのアップデートとaptでnodejsとnpmをインストール

$ sudo apt update
$ sudo apt upgrade
$ sudo apt install node npm
$ node -v
v8.10.0
$ which node
/usr/bin/node
$ npm -v
3.5.1

npm をアップデートしようとすると...?

$ sudo npm install -g npm 
NPM ERR!

# why?

$ which npm
/mnt/c/Program Files/nodejs/npm

Oh! command npm がwsl上にインストールされたnpmで
はなく、ゲストマシンのwindows上にインストールされたnpmを指しています

エイリアスを置きましょう

$ cd ~

# 念のため.bashrcをバックアップ
$ cp .bashrc .bashrc-bak 

$ echo "alias npm=/usr/bin/npm"  >> .bashrc 
$ source .bashrc 
$ sudo npm install -g npm 

これで最新のnpmが入るのですが、npm3 ~ npm6 の間でインストール先に変更があったらしく、
インストール先が/usr/bin/npm => /usr/local/bin/npm になるようです

さっき設定したエイリアスを変更します

.bashrcの末尾を編集

$ nano .bashrc
...
- alias npm=/usr/bin/npm
+ alias npm=/usr/local/bin/npm
$ source .bashrc 
$ npm -v
6.13.1
$ which npm
/usr/local/bin/npm

OK!

nで最新安定板のnodejsをインストール

[参考] Ubuntuに最新のNode.jsを難なくインストールする

$ sudo npm install -g n
$ sudo n stable
$ sudo apt purge -y nodejs
$ exec $SHELL -l
$ echo "export PATH=\$PATH:/usr/local/bin" >> .bashrc
$ source .bashrc 
$ node -v
v12.13.1

OK!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js & Express & MySQL & React でTODOリスト Herokuにデプロイ編

はじめに

前回の続きです。
https://qiita.com/hcr1s/items/0e5970c5af496c221a24

今回は、前回作成したAPIをHerokuへデプロイしていきます。

前提

今回は、Herokuのアカウントを所持しており、クレジット登録していることを前提とします。

Heroku app作成

まずは、git initやheroku createを実行していきます。

# 既にgit initしている場合は省略
$ git init
$ git add .
$ git commit -m 'commit名'

# herokuでアプリ作成
$ heroku create アプリ名
$ heroku git:remote -a アプリ名

$ git push heroku master

とりあえず本番環境へpushは完了です。

MySQL

本番環境でMySQLへ接続するための設定を行なっていきます。

$ heroku addons:add cleardb

Herokuでは、cleardbというクラウドサービスのMySQLを使用できます。

$ heroku config | grep CLEARDB_DATABASE_URL
↓
CLEARDB_DATABASE_URL: mysql://user名:password@host名/database名?reconnect=true

このコマンドで、必要なデータベースの情報を取得できます。
この情報をconfigに設定していきます。

$ heroku config:set DATABASE_URL='mysql://*********?reconnect=true'

mysql://のあとは、grepの結果をそのままペースとしてください。

以上でcleardbの設定は終了です。

index.jsの編集

実際にプログラムに必要なコードを記述していきます。
まずは、前回も書いたcreateConnectionの編集をしていきます。

index.js
--- 省略 ---

const databaseName = 'database名'
const connection = mysql.createConnection({
  host: host名,
  user: user名,
  password: 'password',
  database: 'database名'
})

--- 省略 ---

中身に関しては、MySQLの設定時のgrepを参考に記述していきます。
そして、必要なデータベースやテーブルが存在しない場合、つまり初回に限り実行されるSQLを記述します。

index.js
connection.query('create database if not exists ??;', databaseName)
connection.query('use ??', databaseName)
connection.query('create table if not exists todo_list(id int auto_increment, name varchar(255), isDone boolean, index(id))')

前回も書きましたが、SQL文にフィールドを使用する際は??と記述します。

以上で、コードの編集も終了です。

package.jsonの編集

Herokuにデプロイした際に、package.jsonに記述がないとnpm startが実行されてしまうので編集します。

package.json
{
  "name": "todo-api",
  "version": "1.0.0",
  "description": "",
  "engines":{
    "node": "12.13.0",
    "npm": "6.12.0"
  },
  "main": "index.js",
  "scripts": {
    "start" : "node index.js"
  },
--- 省略 ---

デプロイ

最後にデプロイをして終了です。

実際に使ってみましょう。

スクリーンショット 2019-12-02 18.28.30.png

実際にPOSTを送ってみた際のスクショです。
いい感じですね。

終わり

次回は、このAPIを使用してTODOアプリを開発していきます。
何か間違いがある際はおしらせください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JSConfJP 2019 スライドまとめ(WIP)

行けなかったので行きたい気持ちの供養のためにまとめています。
リンクされていないスライドを見つけたらリクエストいただけると幸いです。

DAY 1 (2019/11/30)

Room A(体育館) Room B(B105) Room C(屋上)
12:00
-
13:00
Open
13:00
-
13:30
Opening talk
13:30
-
14:00
The State of JavaScript
by Raphaël Benitte and Sacha Greif
14:00
-
14:15
Break
14:15
-
14:45
「繋がり」の可視化
by Nadieh Bremer
WebAuthnで実現する安全・快適なログイン
by Eiji Kitamura / えーじ
JavaScript AST プログラミング: 入門とその1歩先へ
by Takuto Wada
14:45
-
15:15
「オープンソース」の定義
by Henry Zhu
覚醒するアクセシビリティ
by Lena Morita
4年分のプロシージャルなJS
by Andy Hall
15:15
-
15:30
Break
15:30
-
16:00
Building and Deploying for the Modern Web with JAMstack
by Guillermo Rauch
Wrap-up: Runtime-friendly JavaScript
by Sho Miyamoto
JS開発者のためのSEOテクニック
by Martin Splitt
16:00
-
16:30
Write What Not How
by Jorge Bucaran
How to Boost Your Code with WebAssembly
by FUJI Goro
Playing Pokémon Together with Node.js
by Samuel Agnew
16:30
-
16:45
Break
16:45
-
17:15
Deno - JavaScript の新たな道筋
by Kitson Kelly
Streams APIをちゃんと理解する
by 加藤 健志
You might also like...
by Maria Clara
17:15
-
17:45
Headers for Hackers
by Andrew Betts
Make it Declarative with React
by Toru Kobayashi
Web Accessibilityのすゝめ
by Nazanin Delam
17:45
-
18:15
Break
18:15
-
18:45
Sponsor talk (mediba, メルカリ, サイボウズ, CureApp)
18:45
-
18:55
予測的 Prefetching によるパフォーマンス改善
by Praveen Yedidi
18:55
-
19:05
Web Components era phase 2
by Yoshiki Shibukawa
19:05
-
19:15
Cache Me If You Can
by Maxi Ferreira
19:15
-
19:25
Node.js でつくる Node.js - WASM/WASI ミニミニコンパイラー
by がねこまさし
19:25
-
19:35
React アプリのライセンス違反について
by dynamis
19:35
-
21:00
Party

DAY 2 (2019/12/01)

Room A(体育館) Room B(B105) Room C(屋上)
10:00
-
11:00
Open
11:00
-
11:30
時間はただの幻想である… JavaScriptにおいては
by Jennifer Wong
InversifyJSを用いたレイヤードアーキテクチャの構築
by 奥野 賢太郎
Vue.js で D3.js を使ったインタラクティブなデータの可視化
by Shirley Wu
11:30
-
12:00
ストリームの人生
by Dominic Tarr
Build and scale multiple Voice application
by using TypeScript

by Hidetaka Okamoto
12:00
-
13:00
Break
11:00
-
13:00
Web の自重
by Jxck
正攻法はあるのか !? 泥臭く戦った Node.js バージョンアップ一部始終
by Masato Nishihara
パスワードは90年代の代物だ
by Sam Bellen
13:30
-
14:00
JavaScript, Rust and Wasm Walk into a Ramen Shop ...
by Irina Shestak
Migration from React Native to PWA
by ohbarye
npm i -g @next-and-beyond: Building the future of package management
by Claudia Hernández
14:00
-
14:15
Break
14:15
-
14:45
JSConf Panel Talks
by Jan Lehnardt and Lena Morita and Mariko Kosaka and Yosuke Furukawa
GraphQLを用いたECサイトにおけるパフォーマンス改善
by 澤井宣彦
Minimum Hands-on Node.js
by 栗山 太希
14:45
-
15:15
悪用された npm パッケージの分析
by Jarrod Overson
JavaScriptのままでTypeScriptを始める
by 高梨ギンペイ
15:15
-
15:30
Break
15:30
-
16:00
Browser APIs: 知られざるヒーロー達
by Rowdy Rabouw
Your benchmark may not guide a real application performance
by Tetsuharu OHZEKI
16:00
-
16:30
Anatomy of a Click
by Benjamin Gruenbaum
JavaScriptとSwift/JavaをつなげるCapacitorと、これからのWeb Frontned.
by 榊原昌彦
Recruit Speed Hackathon
by 新井 智士
16:30
-
16:45
Break
16:45
-
17:15
AIとJavaScript による生物認識
by Jonny Kalambay
大規模アプリケーション開発でのElm実践
by 海老原 圭吾
17:15
-
17:45
Pika: レジストリの再創造
by Fred K. Schott
最新のWeb技術でIoT開発をする
by 木戸 康平
17:45
-
18:00
Break
18:00
-
18:30
Sponsor talk Yahoo! Japan, リクルート, ドワンゴ, DMM, Twilio
18:30
-
19:00
points at random
by Mariko Kosaka
19:00
-
19:30
Closing

Gist

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React App #1 方針決め

TL;DR

ReactでWebアプリを作ります。
構造とか意識しつつ気になるツールを組み込む予定です。(進捗次第)

私について

  • Reactは4年前くらいに触った気がする。記憶のかなた。
  • webも数年前から触っていなかったが、最近また調べ始めた。
  • 最近アーキテクチャを意識し出した。依存性逆転の原則おもしろい。

開発環境

  • Mac OS Catalina
  • Node.js
  • Yarn

方針(アプリの内容)

名前: Rooms(仮)
概要: 色々な部屋?を見るアプリ
操作: 上下左右にフリックorボタンで移動

部屋は作成もできる(作成だけで別のアプリができる分量になりそう...?最初は簡単な部屋)
他の人の作った部屋を閲覧したい...?

方針(開発)

  • SPA
  • スマートフォン向け

アーキテクチャ

データは一方通行で流れるべきらしい。
Reactの設計思想がそうなので、その流れを組んでfluxを意識する。
後述のReduxがフレームワークとして勝手に意識してくれそう?

コンポーネントの作り方はAtomic Designを意識する。

  • flux
  • Atomic Design

サーバーサイド

サービス

Advent Calendarの制約?により、Firebaseだが、最初はローカルの予定なので出番がなさそう。

とりあえずFirebase Hostingで、静的なウェブサイトの構築。
部屋の共有機能などを実装したくなったら、他のサービスも追加していく。

クライアントサイド

CSS

ReactではCSS-ModulesかCSS-in-JSが良さそう。
後述のAnt Designは競合するのか否か。

lint系

Prettierを使ったことがないので、使ってみたい。

その他

  • Redux(定番そう?)
  • Jest + enzyme(テスト書きたい)
  • TypeScript(考え中)
  • React Hook(使いたい)
  • Ant Design(できれば)
  • Containerize(できれば)
  • GitHub Actions に載せる(できれば)
  • PWA(できれば)
  • Clean Architecture(必要そうなら)
  • React Native でアプリに(できれば)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Node.js]Expressのミドルウェアの基本

はじめに

この記事では、Expressを使いこなす上で必須となる、「ミドルウェア」について説明します。Expressをこれから使い始める方、使い始めて間もない方を対象にした基本的な内容になります。

現在の最新バージョンである4.17.1で動作を確認しています。

Expressとミドルウェア

ExpressはNode.js環境で使われる最も人気のあるWebアプリケーションフレームワークのうちの一つです。最小限の機能を提供する薄いフレームワークになっていて、ユーザーの自由度が高い点が特徴的です。(例えば、データベースアクセス、認証をExpressは提供しません。)

また、ドキュメントにある通り、「Expressアプリケーションは一連のミドルウェア関数呼び出し」といえます。つまり、ユーザーからのHTTPリクエストを受信しレスポンスを返すまでに、いくつかのミドルウェア関数を通過し、リクエストからレスポンスまでのサイクルが完了します。

ミドルウェアとは

ミドルウェアとは、リクエストオブジェクトとレスポンスオブジェクトを受け取り、任意の処理を行う関数です。また、「Expressは一連のミドルウェア関数の呼び出し」と言われるように、次のミドルウェアへと処理を継続するか、もしくはリクエストレスポンスサイクルをそこで終了させるかのいずれかを行います。

次の関数を見てください。

/*
 ドキュメントを元に作成
*/
var myLogger = function (req, res, next) {
  // 何か処理を行う
  console.log('LOGGED')

  // 次のミドルウェアに処理を移す
  next()
}

この例では、"LOGGED"の文字列を表示した後、nextを使って、次のミドルウェアへと処理が流れていきます。
実際の使い方とリクエストレスポンスサイクルを終了する例については、この後紹介します。

アプリケーション全体(複数のミドルウェア関数を使う)

次は、二つのミドルウェア関数を使う例です。expressモジュールをインストール済みであれば、このまま実行可能です。(npm install express)

app.js
/*
 ドキュメントを元に作成
*/
var express = require('express')

// アプリケーションオブジェクトの作成
var app = express()

// ミドルウェアの定義
var myLogger = function (req, res, next) {
  console.log('LOGGED')
  // 次のミドルウェアへ
  next()
}

// ミドルウェアの読み込み
app.use(myLogger)

// パスを限定してミドルウェアを適用
app.get('/', function (req, res) {
  // ミドルウェア関数の連鎖はここでおしまい
  res.send('Hello World!')
})

app.listen(3000)

ミドルウェア関数を読み込む順序も重要です。上記の例では、res.sendを含むミドルウェア関数を先に読み込んでしまうと、myLoggerは実行されません。res.sendで一連のサイクルが終了してしまうからです。

サード・パーティー・ミドルウェアを使う

先ほどの例のように自作でミドルウェア関数を作成することができますが、頻繁に行われる処理については、サード・パーティー・ミドルウェアが存在します。

例えば、リクエストボディのJSONへの変換、cookieの読み取り、認証処理、ヘッダーの操作などです。
ここに一覧がまとまっています。

読み込ませ方は、自作ミドルウェアの場合と変わりません。たとえば、"cookie-parser"の場合は次のようになります。

var cookieParser = require('cookie-parser')
app.use(cookieParser())

設定方法等は各モジュールによって違いますので、それぞれのドキュメントを参照してください。

以上です。

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エンジニアの教養、フレームワーク解剖学【Express編】

このエントリは、GMSアドベントカレンダー2日目のものです。
昨日は、Hiroshi OdaさんのAWSでドキュメントサイトを最速で公開する方法。自動デプロイもあるよ!でした。

Node.jsで最も使われているフレームワーク「Express」。職場ではExpressを用いて開発しているのですが、自分にとってExpress内部が完全にブラックボックス化していました。こりゃまずい。ということで、Expressをソースコードから理解してみた際のまとめ記事です。

Expressは最小限で柔軟なアプリケーションフレームワークと言われていることもあり、他のフレームワークと比べてフレームワークの内部挙動を把握しやすいと思います。そのため、Expressを知らない、Node.jsを知らないという方でも、参考になるかと。

この記事を読破すると達成できること

  • 天才が作った人気OSSアプリケーションフレームワークの内部構造を把握できる。
  • Expressのソースコードを読めるようになる。

※ 本記事は、あくまでExpressの内部挙動について解説するものです。Expressの環境の整え方や便利テクニック等について言及するものではありません。

Expressとは?

スクリーンショット 2019-11-30 8.11.18.png
【画像引用】Expressjs.com

まずExpressの特徴を軽く触れておきたいと思います。

Express は、Web アプリケーションとモバイル・アプリケーション向けの一連の堅固な機能を提供する最小限で柔軟な Node.js Web アプリケーション・フレームワークです。
(引用:Expressjs.com)

最小限で柔軟なアプリケーションフレームワークがExpressです。あとで見ていきますが、最小限と謳っているだけあって、ソースコード量も少なく、整理されていて、非常に理解しやすいです。

またExpressは、天才TJ Holowaychuk氏によって作成されたそうです。この方は他にも様々なlibraryを作ってきました。

Stop me if you’ve heard or used any of these libraries before: Express, Connect, Dox, N, Apex, git-extras, Jade, Stylus, Mocha, Superagent, EJS, Co, Koa, Commander, Should…
This is just a partial list of the high profile open source projects created by TJ. And these represent just a fraction of the amount of projects that TJ created or has been involved in.
引用:TJ Holowaychuk

Expressのファイル構成

ExpressのGithubにあるレポジトリのリンク→Express github

とりあえずExpressのファイル構成を見てみましょう。

スクリーンショット 2019-11-26 21.56.17.png

フォルダは以下のような構成になっていることが分かります。

  • benchmarks (ベンチマーク)
  • examples (Expressをどうやって使うかの例文集)
  • lib (Expressの内部ロジック)
  • test (テスト)

Expressの内部ロジックは、libフォルダに全て詰まっています。そしてlibフォルダの中を見ると分かりますが、

libフォルダの中は、たったの11ファイル、、、!!

Expressの内部ロジックが全てこの11ファイルに詰まっているのです!

本記事では、Expressフォルダ直下にあるindex.jsと、Expressを把握する上で重要なlibフォルダ内のファイル5つを中心に、見ていきたいと思います。

以下、それぞれのファイルについての紹介を軽く書いておきました。

express_file.png

ちなみに画像内で、require()という関数名を用いていますが、これはNode.jsのコアモジュールとしてBuilt inされている関数になります。初めて聞いたという方は、Node.jsのAPIDocument「Modules」でrequireの挙動について詳しく解説されているので、参考にしてみてください。

Expressを理解する上で重要な2つの概念

Expressの処理の流れを理解するために、まず2つの概念を理解する必要があります。

  • Middleware
  • Routing

MiddlewareとRouting

とりあえずMiddlewareとRoutingについて、ざっくりと説明します。

例えばサーバーを作るとすると、以下のように考えると思います。

「ユーザーからAリクエストが来たら、A'処理を実行してレスポンスを返したい。」
「ユーザーからBリクエストが来たら、B'処理を実行してレスポンスを返したい。」

この、「ユーザーが何のリクエストをして来たら、どんな処理をしてレスポンスを返すようにしたいのか」を定義するのがRoutingです。そして、リクエストが飛んできた際のその処理は「どんな処理なのか」という内部ロジックを定義するのがMiddlewareです。

順番に詳しく見ていきましょう。

Middleware

Middlewareについてまとめた図を載せておきます。

middleware_chain.jpg

Middlewareとは名前のごとく、「リクエスト-レスポンスサイクルの間(middle)で実行される処理」のことを指します。図が示すように、Middleware関数内でnext()を呼び出してrequestを前に繋いでいくというのが、Expressサーバーの基本スタイルです。

Middlewareのイメージを掴んでいただいたところで、とりあえず公式サイトのMiddlewareの概念説明を見てみましょう。

最後のリクエストハンドラーの前に Express ルーティング層によって呼び出される関数。そのため、未加工要求と最後の目的のルートの間に配置される。
引用:Middleware

もういっちょ。

ミドルウェア 関数は、リクエストオブジェクト (req)、レスポンスオブジェクト (res)、およびアプリケーションのリクエストレスポンスサイクルにおける次のミドルウェア関数に対するアクセス権限を持つ関数です。次のミドルウェア関数は一般的に、next という変数で表されます。
引用:Express アプリケーションで使用するミドルウェアの作成

また、Middlewareは以下の4つのタスクを実行します。

  • 任意のコードを実行する。
  • リクエストオブジェクトとレスポンスオブジェクトを変更する。
  • リクエストレスポンスサイクルを終了する。
  • スタック内の次のミドルウェアを呼び出す。

引用:Express アプリケーションで使用するミドルウェアの作成

また、Middlewareは次の5種類に分けられるそうです。以下で一応引用しておきますが、これはただの概念的なものなので特に覚える必要はありません。ただ、Middlewareの概念の整理のために有用だとは思います。

  • Application level middleware
  • Router level middleware
  • Built-in middleware
  • Error handling middleware
  • Thirdparty middleware

引用:Using Middleware

Routing

ExpressではRoutingを理解する上で以下の3つの概念を理解する必要があります。

  1. Routerオブジェクト(/lib/router/index.jsで定義されている。)
  2. Layerオブジェクト(/lib/router/layer.jsで定義されている。)
  3. Routeオブジェクト(/lib/router/route.jsで定義されている。)

な、なんか難しそう。。。

とりあえずイメージを掴みやすいように図に表してみました。

router.png

それぞれ順に見ていきたいと思います。

Routerオブジェクト

上の図を参考にしながら、まずRouterオブジェクトについて見ていきましょう。

Routerオブジェクトの特徴

  • Routerオブジェクトは、Appオブジェクトの_routerというプロパティに紐づく。
  • Appオブジェクトと、Routerオブジェクトは1対1の関係。
  • Routerオブジェクトは複数のLayerオブジェクトを挿入できるスタック構造のプロパティを持つ。

特に重要なのが、RouterオブジェクトはLayerオブジェクトのstack構造を有しているというところです。

Routerオブジェクトの初期化コード

/lib/router/index.js
/**
 * Initialize a new `Router` with the given `options`.
 *
 * @param {Object} [options]
 * @return {Router} which is an callable function
 * @public
 */

var proto = module.exports = function(options) {
  var opts = options || {};

  function router(req, res, next) {
    router.handle(req, res, next);
  }

  // mixin Router class functions
  setPrototypeOf(router, proto)

  router.params = {};
  router._params = [];
  router.caseSensitive = opts.caseSensitive;
  router.mergeParams = opts.mergeParams;
  router.strict = opts.strict;
  router.stack = [];

  return router;
};

router.stack = [];というコードでrouterオブジェクトのstackプロパティに配列が指定されているのが分かるかと思います。このstackプロパティにLayerオブジェクトが挿入されていきます。

RouterオブジェクトとRouting

Routerオブジェクトと関係している部分のみを抜粋してRouting処理の流れを見てみましょう。

  1. Routerオブジェクトに紐づいているLayerオブジェクトのpathプロパティが、ユーザーからのリクエストのパラメータと一致しているかチェックする。
  2. 一致している場合は、そのLayerオブジェクトのhandlerのmiddleware関数を実行する。
  3. 一致していない場合は、次のLayerオブジェクトに進み、1から再実行

Routerインスタンスの作成

Expressサーバーを作成するには、このRouterオブジェクトを作成する必要があります。では、このRouterインスタンスは、どのようにして作成されるのか簡単に図に表してみました。

router_keiro.png

Routerオブジェクトの作成を担うのは、app.lazyrouter()という関数です。コードを見ておきましょう。

./lib/application.js
app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    this._router.use(query(this.get('query parser fn'))); 
    this._router.use(middleware.init(this));
  }
};

new RouterでRouterオブジェクトを作成していますね。this._router.useの部分については後で別に見ていきたいと思います。

Layerオブジェクト

次はLayerオブジェクトですね。さっきの図をもう一度貼っておくので、今度はLayerオブジェクトに注目して見てください。

router.png

Layerオブジェクトの特徴

  • Routerオブジェクトに紐づくLayerオブジェクト
    • Routerオブジェクトのstackプロパティに挿入されていく。
    • pathとmiddleware関数をプロパティに持つ。
    • Routeオブジェクトが紐づいている場合、middleware関数にはroute.dispatch()が入る。
  • Routeオブジェクトに紐づくLayerオブジェクト
    • Routeオブジェクトのstackプロパティに挿入されていく。
    • methodとmiddleware関数をプロパティに持つ。

Layerオブジェクトの形式

先ほど紹介した図でも示しましたが、Routerオブジェクトに紐づくLayerオブジェクトには3つの形式があり、Routeオブジェクトに紐づくLayerオブジェクトには2つの形式があります。

  • Routerオブジェクトに紐づくLayerオブジェクト
    • path無し + middleware関数
    • path有り + middleware関数
    • path有り + route.dispatch()
  • Routeオブジェクトに紐づくLayerオブジェクト
    • method無し + middleware関数
    • method有り + middleware関数

[path無し + middleware関数]の場合、ユーザーからの全リクエストに対してmiddleware関数が実行されます。内部的には、Layerを登録する際にpath指定が無ければ、デフォルトでpath = '/';となるからです。(参考リンクとして、app.use()というRouterオブジェクトにLayerオブジェクトを挿入するメソッドの、pathがデフォルトで代入されている部分を載せておきます。:app.use())

Layerオブジェクトの初期化コード

./lib/router/layer.js
function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);
  }

  debug('new %o', path)
  var opts = options || {};

  this.handle = fn;
  this.name = fn.name || '<anonymous>';
  this.params = undefined;
  this.path = undefined;
  this.regexp = pathRegexp(path, this.keys = [], opts);

  // set fast path flags
  this.regexp.fast_star = path === '*'
  this.regexp.fast_slash = path === '/' && opts.end === false
}

LayerオブジェクトとRouting

サーバーに届いたリクエストは、Routerオブジェクトに紐づいたLayerオブジェクトのpathプロパティと順に比較されていきます。そしてリクエストのpathとLayerオブジェクトのpathが合致した場合、そのLayerオブジェクトのMiddleware関数がrequestに対して実行されます。そして、また次のLayerオブジェクトに進みます。

ここで重要なのが、Middleware関数がリクエストに対して実行されていく順序は、LayerオブジェクトがRouterオブジェクト、もしくはRouteオブジェクトに挿入された順序であるということです。

要するにExpressサーバーを作成する際には、Layerオブジェクト作成の順序を気にする必要があるということです。

Layerインスタンスの作成

最後にLayerインスタンスがどのようにして作成されるのか内部挙動を追ってみましょう。

layer.png

Routeオブジェクト

最後はRouteオブジェクトです。

router.png

Routeオブジェクトの特徴

  • Routeオブジェクトは、Routerオブジェクトに紐づいたLayerオブジェクトと1対1の関係て紐づく。
  • RouteオブジェクトはRouterオブジェクトと同様に、複数のLayerオブジェクトを挿入できるスタック構造のプロパティを持つ。

Routeインスタンスの作成

route_keiro.png

Routeオブジェクトの初期化処理

./lib/router/route.js
/**
 * Initialize `Route` with the given `path`,
 *
 * @param {String} path
 * @public
 */

function Route(path) {
  this.path = path;
  this.stack = [];

  debug('new %o', path)

  // route handlers for various http methods
  this.methods = {};
}

Routeオブジェクトのpathプロパティに代入されているpathは、Routeオブジェクトに紐づいているLayerオブジェクトのpathと同じpathです。これは、Routeオブジェクトを作成する_router.route()メソッドを読むと分かります。

RouteオブジェクトとRouting

Routeオブジェクトに関係する部分でのRouting処理を抜粋すると以下のような流れになります。

  1. リクエストのpathと、Routerオブジェクトに紐づいているLayerオブジェクトに登録されているpathが一致した。
  2. そのLayerオブジェクトのhandleプロパティがmiddleware関数ではなくroute.dispatch()だった。
  3. そのLayerオブジェクトに紐づいたRouteオブジェクトへと進む
  4. リクエストのhttp method(GETとかPOSTとか)と、Routeオブジェクトに紐づいているLayerオブジェクトに登録されているmethodが一致した場合、そのLayerオブジェクトに登録されているmiddleware関数を実行する。

以上でRoutingの説明が終わり、Expressを理解する上で重要なMiddlewareとRoutingをあなたは理解したことになります!:clap::clap:

ここからはExpressのGithubレポジトリで紹介されているサンプルコードを用いて、実際にどのようにしてExpressサーバーが作成されているのか見ていきたいと思います。ここまでで出てきた概念の確認も兼ねて読んでみて下さい。

Expressのサンプルコードを用いた実践解剖

Expressのgithubレポジトリ上で紹介されているサンプルコードは以下です。

const express = require('express')
const app = express()

app.get('/', function (req, res) {
  res.send('Hello World')
})

app.listen(3000)

【引用】Express Github

なんというシンプルなコード。。。このコードでExpressサーバーが完成します。また、このサンプルコードは、Expressで重要な概念を全て網羅していますし、Expressの内部構造を理解するのに適しています。

では、このサンプルコードを3部に分けて内部挙動を見ていきたいと思います。

1 Expresアプリケーションの作成

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

2 Routing処理・Middleware関数の登録

app.get('/', function (req, res) {
  res.send('Hello World');
});

3 Expressサーバーの作成

app.listen(3000);

1. Expresアプリケーションの作成

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

const express = require('express');で、expressモジュールがimportされています。importで、expressフォルダの最上位層にあるindex.jsが呼ばれます。(requireの挙動については、Modulesを参考にしてみて下さい。)

ではindex.jsを見ていきましょう。

index.js

index.js
'use strict';

module.exports = require('./lib/express');

index.jsには上の2行しか含まれていません。require('./lib/express');で、./lib/expressファイルをimportしていますね。さらに辿って、./lib/expressを見てみましょう。

./lib/express.js

ここで紹介するコードは抜粋されています。

./lib/express.js
/**
 * Module dependencies.
 */

var bodyParser = require('body-parser')
var EventEmitter = require('events').EventEmitter;
var mixin = require('merge-descriptors');
var proto = require('./application');
var Route = require('./router/route');
var Router = require('./router');
var req = require('./request');
var res = require('./response');

/**
 * Expose `createApplication()`.
 */

exports = module.exports = createApplication;

/**
 * Create an express application.
 *
 * @return {Function}
 * @api public
 */

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);

  // expose the prototype that will get set on requests
  app.request = Object.create(req, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  // expose the prototype that will get set on responses
  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  app.init();
  return app;
}

exports = module.exports = createApplication;で、createApplication関数がexportされています。

つまり、const express = require('express');const app = express();の流れは以下のようになります。

  • Expressアプリケーションのコンストラクタ関数であるcreateApplication関数が実行される
  • createApplication関数でreturnされるapp関数がapp変数に代入される。

後で説明しますが、このExpressアプリケーション、つまりapp関数は、サーバーに届いたリクエストに対するリクエストハンドラーとして登録されます。

ちなみに、この./lib/express.jsファイルには他にも様々な記述があり、それらはExpressのAPI Documentでも説明されているので、時間があれば、どんなものがあるか見てみると良いと思います。

2. Routing処理・Middleware関数の登録

さて、前項でExpressアプリケーションの雛形の作成は完了しましたね。しかしまだアプリケーションのルーティング処理が一切実装されていません。内部的に言うとRouterオブジェクトが作成されていないということです。それはまずいので、Routing処理とMiddleware関数を追加しましょう。

それが以下のサンプルコードです。

app.get('/', function (req, res) {
  res.send('Hello World');
});

app.getは./lib/application.jsにコードがあります。ちなみにapplication.jsでは、Expressのアプリケーションのプロトタイプが定義されています。サンプルコードと関係している部分を抜粋して見ていきましょう。

./lib/application.js
/**
 * Application prototype.
 */

var methods = require('methods');

var app = exports = module.exports = {};

/**
 * Delegate `.VERB(...)` calls to `router.VERB(...)`.
 */

methods.forEach(function(method){
  app[method] = function(path){

    //省略

    this.lazyrouter();

    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

var methods = require('methods');で、methodsモジュールがimportされています。これによってmethods変数には、Node.jsがサポートするhttp_method(小文字)の配列が返ってきます。ここではhttp methodをforEachで繰り返し処理しています。

app[method]内では以下の4つの作業が行われています。

  1. Routerオブジェクトの作成
  2. Routerオブジェクトに紐づいたLayerオブジェクトの作成
  3. Layerオブジェクトに紐づいたRouteオブジェクトの作成
  4. Routeオブジェクトに紐づいたLayerオブジェクトの作成

わ、訳が分からん。。。

というわけで、図にしてみました。

app_get.png

ちなみにサンプルコードに即して説明すると、各Layerオブジェクトのプロパティは以下のようになります。

  • Routerオブジェクトに紐づいたLayerオブジェクト
    • pathプロパティの値は、'/'
    • handlerは、route,dispatch()
  • Routeオブジェクトに紐づいたLayerオブジェクト
    • methodプロパティの値は、'get'
    • handlerは、function (req, res) {res.send('Hello World');}

3. Expressサーバーの作成

ここまででRouting処理、Middleware関数の登録が行われているExpressアプリケーションが完成しています。ですが、ユーザーからのリクエストに対してExpressアプリケーションを実行するように命令する処理がまだありません。それが以下のサンプルコードで定義されています。

app.listen(3000)

このコードに関連した部分を抜粋した内部コードは以下です。

./lib/application.js
var http = require('http');

/**
 * Application prototype.
 */

var app = exports = module.exports = {};

app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

まず、var server = http.createServer(this);について。

Node.jsのコアモジュールであるhttpモジュールがimportされ、http.createServer(requestListener)というメソッドがapp変数を引数として内部で実行されています。これによって、簡易サーバーが作成され、ユーザーからのリクエストに対してapp変数(これは先ほど説明しましたが、createApplication()関数のことです。)が実行されるようになります。

そして、return server.listen.apply(server, arguments);ですね。
これもhttpモジュールで定義されているserver.listen()メソッドが使用されています。これによって、サンプルコード上で引数として渡されたポート番号3000を使用したサーバーが起動します。

つまり、まとめるとapp.listen(3000)というコードは内部的には以下の2つの処理を行っています。

  1. リクエストに対してExpressアプリケーションを返すサーバーの作成
  2. そのサーバーの起動

Expressサーバーを作成する流れのまとめ

以上でサンプルコードを辿る作業が終わりました!:clap::clap:

サンプルコードから分かる、Expressサーバーを作成する際の流れをまとめておきます。

  1. Expressアプリケーションの雛形作成
  2. ExpressアプリケーションにRouting処理・Middlewareの追加
  3. Expressサーバーの作成・起動

Expressを理解するプラスアルファ集

middlewareフォルダ

特に重要ではないのですが、libフォルダ内で一度も触れていない./lib/middlewareフォルダについて軽く見ておきたいと思います。middlewareフォルダの中には以下の2つのファイルが入っています。

  • middleware
    • init.js
    • query.js

middlewareフォルダのファイルは何のために存在するのか

まず、その存在理由について見ていきましょう。init.jsとquery.jsはどこで使われているのか。

これらは、前に説明した、Routerオブジェクトを作成する関数「app.lazyrouter()」の中で使われています。

./lib/application.js
var middleware = require('./middleware/init');
var query = require('./middleware/query');

app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    this._router.use(query(this.get('query parser fn')));
    this._router.use(middleware.init(this));
  }
};

ちなみに、app.lazyrouter()は、唯一Routerオブジェクトを作成する機能を持つメソッドでしたね。

routerオブジェクトのuseメソッド(_router.use())は、layerを作成し、作成したlayerをrouterのstackプロパティにpushするメソッドです。

つまり、app.lazyrouter()が呼び出されると、処理の流れは以下のようになります。

  1. Routerオブジェクトの作成
  2. query関数をhandlerとして持つLayerオブジェクトの作成、そして作成されたLayerオブジェクトをRouterオブジェクトのstackプロパティに挿入する
  3. middleware.init関数をhandlerとして持つLayerオブジェクトの作成、そして作成されたLayerオブジェクトをRouterオブジェクトのstackプロパティに挿入する

つまり、query関数とmiddleware.init関数は、Routerオブジェクトに最初に挿入される「始祖のlayer」のmiddleware関数になります。そしてそれらは、ユーザーからの全てのリクエストに対して一番最初に実行されるというわけです。

router_middleware.png

query関数が定義されているのは./middleware/query.jsであり、middleware.init関数が定義されているのは./middleware/init.jsです。

順番にそれぞれのファイルを見ていきたいと思います。

./lib/middleware/query.js

コードを見てみましょう。

./lib/middleware/query.js
var parseUrl = require('parseurl');
var qs = require('qs');

module.exports = function query(options) {
  //省略
  var queryparse = qs.parse;

  //省略

  return function query(req, res, next){
    if (!req.query) {
      var val = parseUrl(req).query;
      req.query = queryparse(val, opts);
    }

    next();
  };
};

next()が関数内の最後に定義されていることからも、このqueryという関数がmiddleware関数として使われていることが分かります。

このexportされたquery関数が、前に見た始祖のLayerオブジェクトのhandlerになっているわけですね。このqueryメソッドの主な処理の流れは以下のようになります。

  1. requestのurlをparseして、queryを取得
  2. 取得したqueryをオブジェクトへとparseして、requestのqueryプロパティに代入

var val = parseUrl(req).query;では、parseUrlモジュールが使われていますね。parseUrl(req)でrequestオブジェクトのurlプロパティがparseされ、そのqueryプロパティがvalに代入されています。

そしてvar queryparse = qs.parse;req.query = queryparse(val, opts);では、qsモジュールが使われていますね。(ちなみにこのqsモジュールもExpressの生みの親であるTJ Holowaychukが作ったモジュールだそうです。天才かよ!)

stringのqueryが入っているvalをqueryparse()でオブジェクトへとparseしています。そしてrequestのqueryプロパティに代入しています。

では何故このquery関数を全リクエストに対して実行させているのか。推測ですが、リクエストのqueryをオブジェクトとして簡単に参照できるようにするためでしょう。実際この処理のおかげで、例えば'index.js?foo=bar'といったurlに対してリクエストがきた場合、req.query.fooと書くことで簡単にbarが取り出せるようになりました。

./lib/middleware/init.js

続いてinit.jsについても見ていきましょう。コードは以下です。

./lib/middleware/init.js
exports.init = function(app){
  return function expressInit(req, res, next){
    if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');

    //省略

    next();
  };
};

if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');というコードから、responseのヘッダーに('X-Powered-By', 'Express')を適用していることが分かります。

このコードが中々有害で、セキュリティ的によろしくないそうです。Expressの公式サイトでもセキュリティ対策として、このコードを打ち消すコードを追加しろって言っています。(自分でソースコードに入れといて、セキュリティやばいって勧告している。。。。)

At a minimum, disable X-Powered-By header
If you don’t want to use Helmet, then at least disable the X-Powered-By header. Attackers can use this header (which is enabled by default) to detect apps running Express and then launch specifically-targeted attacks.
引用:Production Best Practices: Security

最後に

少し時間足らずで、本記事ではExpressを最後まで深掘りできませんでした。もしこの記事が好評なら続編出してみたいな。

あと誤りあれば、ご一報いただけると嬉しいです?‍♂️

明日のアドベントカレンダーの担当はkooogeさんです!

参考

Express Github
Express API Reference
Node.js http Documentation
Understanding Expressjs
Express tutorial for ExpressJS
How Node JS middleware Works?
How express.js works - Understanding the internals of the express library
Express.js under the hood

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エンジニアなら知っておくべき教養、フレームワーク解剖学【Express編】

このエントリは、GMSアドベントカレンダー2日目のものです。
昨日は、Hiroshi OdaさんのAWSでドキュメントサイトを最速で公開する方法。自動デプロイもあるよ!でした。

Node.jsで最も使われているフレームワーク「Express」。職場ではExpressを用いて開発しているのですが、自分にとってExpress内部が完全にブラックボックス化していました。こりゃまずい。ということで、Expressをソースコードから理解してみた際のまとめ記事です。

Expressは最小限で柔軟なアプリケーションフレームワークと言われていることもあり、他のフレームワークと比べてフレームワークの内部挙動を把握しやすいと思います。そのため、Expressを知らない、Node.jsを知らないという方でも、参考になるかと。

この記事を読破すると達成できること

  • 天才が作った人気OSSアプリケーションフレームワークの内部構造を把握できる。
  • Expressのソースコードを読めるようになる。

※ 本記事は、あくまでExpressの内部挙動について解説するものです。Expressの環境の整え方や便利テクニック等について言及するものではありません。

Expressとは?

スクリーンショット 2019-11-30 8.11.18.png
【画像引用】Expressjs.com

まずExpressの特徴を軽く触れておきたいと思います。

Express は、Web アプリケーションとモバイル・アプリケーション向けの一連の堅固な機能を提供する最小限で柔軟な Node.js Web アプリケーション・フレームワークです。
(引用:Expressjs.com)

最小限で柔軟なアプリケーションフレームワークがExpressです。あとで見ていきますが、最小限と謳っているだけあって、ソースコード量も少なく、整理されていて、非常に理解しやすいです。

またExpressは、天才TJ Holowaychuk氏によって作成されたそうです。この方は他にも様々なlibraryを作ってきました。

Stop me if you’ve heard or used any of these libraries before: Express, Connect, Dox, N, Apex, git-extras, Jade, Stylus, Mocha, Superagent, EJS, Co, Koa, Commander, Should…
This is just a partial list of the high profile open source projects created by TJ. And these represent just a fraction of the amount of projects that TJ created or has been involved in.
引用:TJ Holowaychuk

Expressのファイル構成

ExpressのGithubにあるレポジトリのリンク→Express github

とりあえずExpressのファイル構成を見てみましょう。

スクリーンショット 2019-11-26 21.56.17.png

フォルダは以下のような構成になっていることが分かります。

  • benchmarks (ベンチマーク)
  • examples (Expressをどうやって使うかの例文集)
  • lib (Expressの内部ロジック)
  • test (テスト)

Expressの内部ロジックは、libフォルダに全て詰まっています。そしてlibフォルダの中を見ると分かりますが、

libフォルダの中は、たったの11ファイル、、、!!

Expressの内部ロジックが全てこの11ファイルに詰まっているのです!

本記事では、Expressフォルダ直下にあるindex.jsと、Expressを把握する上で重要なlibフォルダ内のファイル5つを中心に、見ていきたいと思います。

以下、それぞれのファイルについての紹介を軽く書いておきました。

express_file.png

ちなみに画像内で、require()という関数名を用いていますが、これはNode.jsのコアモジュールとしてBuilt inされている関数になります。初めて聞いたという方は、Node.jsのAPIDocument「Modules」でrequireの挙動について詳しく解説されているので、参考にしてみてください。

Expressを理解する上で重要な2つの概念

Expressの処理の流れを理解するために、まず2つの概念を理解する必要があります。

  • Middleware
  • Routing

MiddlewareとRouting

とりあえずMiddlewareとRoutingについて、ざっくりと説明します。

例えばサーバーを作るとすると、以下のように考えると思います。

「ユーザーからAリクエストが来たら、A'処理を実行してレスポンスを返したい。」
「ユーザーからBリクエストが来たら、B'処理を実行してレスポンスを返したい。」

この、「ユーザーが何のリクエストをして来たら、どんな処理をしてレスポンスを返すようにしたいのか」を定義するのがRoutingです。そして、リクエストが飛んできた際のその処理は「どんな処理なのか」という内部ロジックを定義するのがMiddlewareです。

順番に詳しく見ていきましょう。

Middleware

Middlewareについてまとめた図を載せておきます。

middleware_chain.jpg

Middlewareとは名前のごとく、「リクエスト-レスポンスサイクルの間(middle)で実行される処理」のことを指します。図が示すように、Middleware関数内でnext()を呼び出してrequestを前に繋いでいくというのが、Expressサーバーの基本スタイルです。

Middlewareのイメージを掴んでいただいたところで、とりあえず公式サイトのMiddlewareの概念説明を見てみましょう。

最後のリクエストハンドラーの前に Express ルーティング層によって呼び出される関数。そのため、未加工要求と最後の目的のルートの間に配置される。
引用:Middleware

もういっちょ。

ミドルウェア 関数は、リクエストオブジェクト (req)、レスポンスオブジェクト (res)、およびアプリケーションのリクエストレスポンスサイクルにおける次のミドルウェア関数に対するアクセス権限を持つ関数です。次のミドルウェア関数は一般的に、next という変数で表されます。
引用:Express アプリケーションで使用するミドルウェアの作成

また、Middlewareは以下の4つのタスクを実行します。

  • 任意のコードを実行する。
  • リクエストオブジェクトとレスポンスオブジェクトを変更する。
  • リクエストレスポンスサイクルを終了する。
  • スタック内の次のミドルウェアを呼び出す。

引用:Express アプリケーションで使用するミドルウェアの作成

また、Middlewareは次の5種類に分けられるそうです。以下で一応引用しておきますが、これはただの概念的なものなので特に覚える必要はありません。ただ、Middlewareの概念の整理のために有用だとは思います。

  • Application level middleware
  • Router level middleware
  • Built-in middleware
  • Error handling middleware
  • Thirdparty middleware

引用:Using Middleware

Routing

ExpressではRoutingを理解する上で以下の3つの概念を理解する必要があります。

  1. Routerオブジェクト(/lib/router/index.jsで定義されている。)
  2. Layerオブジェクト(/lib/router/layer.jsで定義されている。)
  3. Routeオブジェクト(/lib/router/route.jsで定義されている。)

な、なんか難しそう。。。

とりあえずイメージを掴みやすいように図に表してみました。

router.png

それぞれ順に見ていきたいと思います。

Routerオブジェクト

上の図を参考にしながら、まずRouterオブジェクトについて見ていきましょう。

Routerオブジェクトの特徴

  • Routerオブジェクトは、Appオブジェクトの_routerというプロパティに紐づく。
  • Appオブジェクトと、Routerオブジェクトは1対1の関係。
  • Routerオブジェクトは複数のLayerオブジェクトを挿入できるスタック構造のプロパティを持つ。

特に重要なのが、RouterオブジェクトはLayerオブジェクトのstack構造を有しているというところです。

Routerオブジェクトの初期化コード

/lib/router/index.js
/**
 * Initialize a new `Router` with the given `options`.
 *
 * @param {Object} [options]
 * @return {Router} which is an callable function
 * @public
 */

var proto = module.exports = function(options) {
  var opts = options || {};

  function router(req, res, next) {
    router.handle(req, res, next);
  }

  // mixin Router class functions
  setPrototypeOf(router, proto)

  router.params = {};
  router._params = [];
  router.caseSensitive = opts.caseSensitive;
  router.mergeParams = opts.mergeParams;
  router.strict = opts.strict;
  router.stack = [];

  return router;
};

router.stack = [];というコードでrouterオブジェクトのstackプロパティに配列が指定されているのが分かるかと思います。このstackプロパティにLayerオブジェクトが挿入されていきます。

RouterオブジェクトとRouting

Routerオブジェクトと関係している部分のみを抜粋してRouting処理の流れを見てみましょう。

  1. Routerオブジェクトに紐づいているLayerオブジェクトのpathプロパティが、ユーザーからのリクエストのパラメータと一致しているかチェックする。
  2. 一致している場合は、そのLayerオブジェクトのhandlerのmiddleware関数を実行する。
  3. 一致していない場合は、次のLayerオブジェクトに進み、1から再実行

Routerインスタンスの作成

Expressサーバーを作成するには、このRouterオブジェクトを作成する必要があります。では、このRouterインスタンスは、どのようにして作成されるのか簡単に図に表してみました。

router_keiro.png

Routerオブジェクトの作成を担うのは、app.lazyrouter()という関数です。コードを見ておきましょう。

./lib/application.js
app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    this._router.use(query(this.get('query parser fn'))); 
    this._router.use(middleware.init(this));
  }
};

new RouterでRouterオブジェクトを作成していますね。this._router.useの部分については後で別に見ていきたいと思います。

Layerオブジェクト

次はLayerオブジェクトですね。さっきの図をもう一度貼っておくので、今度はLayerオブジェクトに注目して見てください。

router.png

Layerオブジェクトの特徴

  • Routerオブジェクトに紐づくLayerオブジェクト
    • Routerオブジェクトのstackプロパティに挿入されていく。
    • pathとmiddleware関数をプロパティに持つ。
    • Routeオブジェクトが紐づいている場合、middleware関数にはroute.dispatch()が入る。
  • Routeオブジェクトに紐づくLayerオブジェクト
    • Routeオブジェクトのstackプロパティに挿入されていく。
    • methodとmiddleware関数をプロパティに持つ。

Layerオブジェクトの形式

先ほど紹介した図でも示しましたが、Routerオブジェクトに紐づくLayerオブジェクトには3つの形式があり、Routeオブジェクトに紐づくLayerオブジェクトには2つの形式があります。

  • Routerオブジェクトに紐づくLayerオブジェクト
    • path無し + middleware関数
    • path有り + middleware関数
    • path有り + route.dispatch()
  • Routeオブジェクトに紐づくLayerオブジェクト
    • method無し + middleware関数
    • method有り + middleware関数

[path無し + middleware関数]の場合、ユーザーからの全リクエストに対してmiddleware関数が実行されます。内部的には、Layerを登録する際にpath指定が無ければ、デフォルトでpath = '/';となるからです。(参考リンクとして、app.use()というRouterオブジェクトにLayerオブジェクトを挿入するメソッドの、pathがデフォルトで代入されている部分を載せておきます。:app.use())

Layerオブジェクトの初期化コード

./lib/router/layer.js
function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);
  }

  debug('new %o', path)
  var opts = options || {};

  this.handle = fn;
  this.name = fn.name || '<anonymous>';
  this.params = undefined;
  this.path = undefined;
  this.regexp = pathRegexp(path, this.keys = [], opts);

  // set fast path flags
  this.regexp.fast_star = path === '*'
  this.regexp.fast_slash = path === '/' && opts.end === false
}

LayerオブジェクトとRouting

サーバーに届いたリクエストは、Routerオブジェクトに紐づいたLayerオブジェクトのpathプロパティと順に比較されていきます。そしてリクエストのpathとLayerオブジェクトのpathが合致した場合、そのLayerオブジェクトのMiddleware関数がrequestに対して実行されます。そして、また次のLayerオブジェクトに進みます。

ここで重要なのが、Middleware関数がリクエストに対して実行されていく順序は、LayerオブジェクトがRouterオブジェクト、もしくはRouteオブジェクトに挿入された順序であるということです。

要するにExpressサーバーを作成する際には、Layerオブジェクト作成の順序を気にする必要があるということです。

Layerインスタンスの作成

最後にLayerインスタンスがどのようにして作成されるのか内部挙動を追ってみましょう。

layer.png

Routeオブジェクト

最後はRouteオブジェクトです。

router.png

Routeオブジェクトの特徴

  • Routeオブジェクトは、Routerオブジェクトに紐づいたLayerオブジェクトと1対1の関係て紐づく。
  • RouteオブジェクトはRouterオブジェクトと同様に、複数のLayerオブジェクトを挿入できるスタック構造のプロパティを持つ。

Routeインスタンスの作成

route_keiro.png

Routeオブジェクトの初期化処理

./lib/router/route.js
/**
 * Initialize `Route` with the given `path`,
 *
 * @param {String} path
 * @public
 */

function Route(path) {
  this.path = path;
  this.stack = [];

  debug('new %o', path)

  // route handlers for various http methods
  this.methods = {};
}

Routeオブジェクトのpathプロパティに代入されているpathは、Routeオブジェクトに紐づいているLayerオブジェクトのpathと同じpathです。これは、Routeオブジェクトを作成する_router.route()メソッドを読むと分かります。

RouteオブジェクトとRouting

Routeオブジェクトに関係する部分でのRouting処理を抜粋すると以下のような流れになります。

  1. リクエストのpathと、Routerオブジェクトに紐づいているLayerオブジェクトに登録されているpathが一致した。
  2. そのLayerオブジェクトのhandleプロパティがmiddleware関数ではなくroute.dispatch()だった。
  3. そのLayerオブジェクトに紐づいたRouteオブジェクトへと進む
  4. リクエストのhttp method(GETとかPOSTとか)と、Routeオブジェクトに紐づいているLayerオブジェクトに登録されているmethodが一致した場合、そのLayerオブジェクトに登録されているmiddleware関数を実行する。

以上でRoutingの説明が終わり、Expressを理解する上で重要なMiddlewareとRoutingをあなたは理解したことになります!:clap::clap:

ここからはExpressのGithubレポジトリで紹介されているサンプルコードを用いて、実際にどのようにしてExpressサーバーが作成されているのか見ていきたいと思います。ここまでで出てきた概念の確認も兼ねて読んでみて下さい。

Expressのサンプルコードを用いた実践解剖

Expressのgithubレポジトリ上で紹介されているサンプルコードは以下です。

const express = require('express')
const app = express()

app.get('/', function (req, res) {
  res.send('Hello World')
})

app.listen(3000)

【引用】Express Github

なんというシンプルなコード。。。このコードでExpressサーバーが完成します。また、このサンプルコードは、Expressで重要な概念を全て網羅していますし、Expressの内部構造を理解するのに適しています。

では、このサンプルコードを3部に分けて内部挙動を見ていきたいと思います。

1 Expresアプリケーションの作成

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

2 Routing処理・Middleware関数の登録

app.get('/', function (req, res) {
  res.send('Hello World');
});

3 Expressサーバーの作成

app.listen(3000);

1. Expresアプリケーションの作成

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

const express = require('express');で、expressモジュールがimportされています。importで、expressフォルダの最上位層にあるindex.jsが呼ばれます。(requireの挙動については、Modulesを参考にしてみて下さい。)

ではindex.jsを見ていきましょう。

index.js

index.js
'use strict';

module.exports = require('./lib/express');

index.jsには上の2行しか含まれていません。require('./lib/express');で、./lib/expressファイルをimportしていますね。さらに辿って、./lib/expressを見てみましょう。

./lib/express.js

ここで紹介するコードは抜粋されています。

./lib/express.js
/**
 * Module dependencies.
 */

var bodyParser = require('body-parser')
var EventEmitter = require('events').EventEmitter;
var mixin = require('merge-descriptors');
var proto = require('./application');
var Route = require('./router/route');
var Router = require('./router');
var req = require('./request');
var res = require('./response');

/**
 * Expose `createApplication()`.
 */

exports = module.exports = createApplication;

/**
 * Create an express application.
 *
 * @return {Function}
 * @api public
 */

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);

  // expose the prototype that will get set on requests
  app.request = Object.create(req, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  // expose the prototype that will get set on responses
  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  app.init();
  return app;
}

exports = module.exports = createApplication;で、createApplication関数がexportされています。

つまり、const express = require('express');const app = express();の流れは以下のようになります。

  • Expressアプリケーションのコンストラクタ関数であるcreateApplication関数が実行される
  • createApplication関数でreturnされるapp関数がapp変数に代入される。

後で説明しますが、このExpressアプリケーション、つまりapp関数は、サーバーに届いたリクエストに対するリクエストハンドラーとして登録されます。

ちなみに、この./lib/express.jsファイルには他にも様々な記述があり、それらはExpressのAPI Documentでも説明されているので、時間があれば、どんなものがあるか見てみると良いと思います。

2. Routing処理・Middleware関数の登録

さて、前項でExpressアプリケーションの雛形の作成は完了しましたね。しかしまだアプリケーションのルーティング処理が一切実装されていません。内部的に言うとRouterオブジェクトが作成されていないということです。それはまずいので、Routing処理とMiddleware関数を追加しましょう。

それが以下のサンプルコードです。

app.get('/', function (req, res) {
  res.send('Hello World');
});

app.getは./lib/application.jsにコードがあります。ちなみにapplication.jsでは、Expressのアプリケーションのプロトタイプが定義されています。サンプルコードと関係している部分を抜粋して見ていきましょう。

./lib/application.js
/**
 * Application prototype.
 */

var methods = require('methods');

var app = exports = module.exports = {};

/**
 * Delegate `.VERB(...)` calls to `router.VERB(...)`.
 */

methods.forEach(function(method){
  app[method] = function(path){

    //省略

    this.lazyrouter();

    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

var methods = require('methods');で、methodsモジュールがimportされています。これによってmethods変数には、Node.jsがサポートするhttp_method(小文字)の配列が返ってきます。ここではhttp methodをforEachで繰り返し処理しています。

app[method]内では以下の4つの作業が行われています。

  1. Routerオブジェクトの作成
  2. Routerオブジェクトに紐づいたLayerオブジェクトの作成
  3. Layerオブジェクトに紐づいたRouteオブジェクトの作成
  4. Routeオブジェクトに紐づいたLayerオブジェクトの作成

わ、訳が分からん。。。

というわけで、図にしてみました。

app_get.png

ちなみにサンプルコードに即して説明すると、各Layerオブジェクトのプロパティは以下のようになります。

  • Routerオブジェクトに紐づいたLayerオブジェクト
    • pathプロパティの値は、'/'
    • handlerは、route,dispatch()
  • Routeオブジェクトに紐づいたLayerオブジェクト
    • methodプロパティの値は、'get'
    • handlerは、function (req, res) {res.send('Hello World');}

3. Expressサーバーの作成

ここまででRouting処理、Middleware関数の登録が行われているExpressアプリケーションが完成しています。ですが、ユーザーからのリクエストに対してExpressアプリケーションを実行するように命令する処理がまだありません。それが以下のサンプルコードで定義されています。

app.listen(3000)

このコードに関連した部分を抜粋した内部コードは以下です。

./lib/application.js
var http = require('http');

/**
 * Application prototype.
 */

var app = exports = module.exports = {};

app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

まず、var server = http.createServer(this);について。

Node.jsのコアモジュールであるhttpモジュールがimportされ、http.createServer(requestListener)というメソッドがapp変数を引数として内部で実行されています。これによって、簡易サーバーが作成され、ユーザーからのリクエストに対してapp変数(これは先ほど説明しましたが、createApplication()関数のことです。)が実行されるようになります。

そして、return server.listen.apply(server, arguments);ですね。
これもhttpモジュールで定義されているserver.listen()メソッドが使用されています。これによって、サンプルコード上で引数として渡されたポート番号3000を使用したサーバーが起動します。

つまり、まとめるとapp.listen(3000)というコードは内部的には以下の2つの処理を行っています。

  1. リクエストに対してExpressアプリケーションを返すサーバーの作成
  2. そのサーバーの起動

Expressサーバーを作成する流れのまとめ

以上でサンプルコードを辿る作業が終わりました!:clap::clap:

サンプルコードから分かる、Expressサーバーを作成する際の流れをまとめておきます。

  1. Expressアプリケーションの雛形作成
  2. ExpressアプリケーションにRouting処理・Middlewareの追加
  3. Expressサーバーの作成・起動

Expressを理解するプラスアルファ集

middlewareフォルダ

特に重要ではないのですが、libフォルダ内で一度も触れていない./lib/middlewareフォルダについて軽く見ておきたいと思います。middlewareフォルダの中には以下の2つのファイルが入っています。

  • middleware
    • init.js
    • query.js

middlewareフォルダのファイルは何のために存在するのか

まず、その存在理由について見ていきましょう。init.jsとquery.jsはどこで使われているのか。

これらは、前に説明した、Routerオブジェクトを作成する関数「app.lazyrouter()」の中で使われています。

./lib/application.js
var middleware = require('./middleware/init');
var query = require('./middleware/query');

app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    this._router.use(query(this.get('query parser fn')));
    this._router.use(middleware.init(this));
  }
};

ちなみに、app.lazyrouter()は、唯一Routerオブジェクトを作成する機能を持つメソッドでしたね。

routerオブジェクトのuseメソッド(_router.use())は、layerを作成し、作成したlayerをrouterのstackプロパティにpushするメソッドです。

つまり、app.lazyrouter()が呼び出されると、処理の流れは以下のようになります。

  1. Routerオブジェクトの作成
  2. query関数をhandlerとして持つLayerオブジェクトの作成、そして作成されたLayerオブジェクトをRouterオブジェクトのstackプロパティに挿入する
  3. middleware.init関数をhandlerとして持つLayerオブジェクトの作成、そして作成されたLayerオブジェクトをRouterオブジェクトのstackプロパティに挿入する

つまり、query関数とmiddleware.init関数は、Routerオブジェクトに最初に挿入される「始祖のlayer」のmiddleware関数になります。そしてそれらは、ユーザーからの全てのリクエストに対して一番最初に実行されるというわけです。

router_middleware.png

query関数が定義されているのは./middleware/query.jsであり、middleware.init関数が定義されているのは./middleware/init.jsです。

順番にそれぞれのファイルを見ていきたいと思います。

./lib/middleware/query.js

コードを見てみましょう。

./lib/middleware/query.js
var parseUrl = require('parseurl');
var qs = require('qs');

module.exports = function query(options) {
  //省略
  var queryparse = qs.parse;

  //省略

  return function query(req, res, next){
    if (!req.query) {
      var val = parseUrl(req).query;
      req.query = queryparse(val, opts);
    }

    next();
  };
};

next()が関数内の最後に定義されていることからも、このqueryという関数がmiddleware関数として使われていることが分かります。

このexportされたquery関数が、前に見た始祖のLayerオブジェクトのhandlerになっているわけですね。このqueryメソッドの主な処理の流れは以下のようになります。

  1. requestのurlをparseして、queryを取得
  2. 取得したqueryをオブジェクトへとparseして、requestのqueryプロパティに代入

var val = parseUrl(req).query;では、parseUrlモジュールが使われていますね。parseUrl(req)でrequestオブジェクトのurlプロパティがparseされ、そのqueryプロパティがvalに代入されています。

そしてvar queryparse = qs.parse;req.query = queryparse(val, opts);では、qsモジュールが使われていますね。(ちなみにこのqsモジュールもExpressの生みの親であるTJ Holowaychukが作ったモジュールだそうです。天才かよ!)

stringのqueryが入っているvalをqueryparse()でオブジェクトへとparseしています。そしてrequestのqueryプロパティに代入しています。

では何故このquery関数を全リクエストに対して実行させているのか。推測ですが、リクエストのqueryをオブジェクトとして簡単に参照できるようにするためでしょう。実際この処理のおかげで、例えば'index.js?foo=bar'といったurlに対してリクエストがきた場合、req.query.fooと書くことで簡単にbarが取り出せるようになりました。

./lib/middleware/init.js

続いてinit.jsについても見ていきましょう。コードは以下です。

./lib/middleware/init.js
exports.init = function(app){
  return function expressInit(req, res, next){
    if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');

    //省略

    next();
  };
};

if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');というコードから、responseのヘッダーに('X-Powered-By', 'Express')を適用していることが分かります。

このコードが中々有害で、セキュリティ的によろしくないそうです。Expressの公式サイトでもセキュリティ対策として、このコードを打ち消すコードを追加しろって言っています。(自分でソースコードに入れといて、セキュリティやばいって勧告している。。。。)

At a minimum, disable X-Powered-By header
If you don’t want to use Helmet, then at least disable the X-Powered-By header. Attackers can use this header (which is enabled by default) to detect apps running Express and then launch specifically-targeted attacks.
引用:Production Best Practices: Security

最後に

少し時間足らずで、本記事ではExpressを最後まで深掘りできませんでした。もしこの記事が好評なら続編出してみたいな。

あと誤りあれば、ご一報いただけると嬉しいです?‍♂️

明日のアドベントカレンダーの担当はkooogeさんです!

参考

Express Github
Express API Reference
Node.js http Documentation
Understanding Expressjs
Express tutorial for ExpressJS
How Node JS middleware Works?
How express.js works - Understanding the internals of the express library
Express.js under the hood

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React-Native環境構築

React Nativeをやってみようと思い、環境構築をした話

jsもあまり書いたことないのですが、React Nativeで遊んで見ようかと思って、
環境作ってみました。

環境

MacOS 10.14.6(mojave)
MacBook Pro(2015)

最初にやったこと

よくわからなかったので、サイトを参考にして、見よう見真似でやってみた。

$ brew install node
$ brew install watchman

watchmanのところで、pythonのインストールが勝手に走ってる、、、
すでにpyenvで各種入れてるのだけど、、、(まあ、/usr/local/binに入れる分にはいいか)

$ npm install -g react-native-cli

この辺までは普通に通るのだが、、、、

$ react-native init ProjectName

ここで、やたらとWARNやらエラーやらが、、、

# ----中略-----
> fsevents@1.2.9 install /Users/seiketomoaki/Documents/react-native/ProjectName/node_modules/fsevents
> node install

node-pre-gyp WARN Using needle for node-pre-gyp https download 
node-pre-gyp WARN Tried to download(404): https://fsevents-binaries.s3-us-west-2.amazonaws.com/v1.2.9/fse-v1.2.9-node-v79-darwin-x64.tar.gz 
node-pre-gyp WARN Pre-built binaries not found for fsevents@1.2.9 and node@13.2.0 (node-v79 ABI, unknown) (falling back to source compile with node-gyp) 
Traceback (most recent call last):
  File "/usr/local/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py", line 50, in <module>
    sys.exit(gyp.script_main())
  File "/usr/local/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 554, in script_main
# ----中略-----
gyp ERR! configure error 
gyp ERR! stack Error: `gyp` failed with exit code: 1
gyp ERR! stack     at ChildProcess.onCpExit 

他に大量に出てるのだが、なんかpythonのエラーじゃん、、、、
で、gyp(?)がエラーとかわけわかりませんが、、、、
そもそもgypって何だ?
早くも暗雲が、、、

これってバージョンが新しすぎるとかか。
しかし、バージョン複数入れられるような形にしなかったしな、、、
一旦、削除することに。

$ npm uninstall -g react-native-cli #これをしとかないと、/usr/local/lib/node_modules/が消えない。
$ brew uninstall watchman #これはやらなくても、問題なかった模様
$ brew uninstall node

今度はnvmを利用してインストールする。

$ brew install nvm
$ mkdir ~/.nvm
# 以下は、.bash_profileに記載
 export NVM_DIR="$HOME/.nvm"
[ -s "/usr/local/opt/nvm/nvm.sh" ] && . "/usr/local/opt/nvm/nvm.sh" 
[[ -r "/usr/local/etc/profile.d/bash_completion.sh" ]] && . "/usr/local/etc/profile.d/bash_completion.sh"
# 取り込む
$ source ~/.bash_profile
# 確認
$ nvm --version
0.35.1
# nodeのインストール
$ nvm install --lts #LTSの最新取り込み
# $HOME/.nvm以下にインストールされた。
$ nvm list
->     v12.13.1
default -> lts/* (-> v12.13.1)
node -> stable (-> v12.13.1) (default)
stable -> 12.13 (-> v12.13.1) (default)
iojs -> N/A (default)
unstable -> N/A (default)
lts/* -> lts/erbium (-> v12.13.1)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.16.2 (-> N/A)
lts/dubnium -> v10.17.0 (-> N/A)
lts/erbium -> v12.13.1
# npmをバージョンアップ
$ npm update -g npm
$ npm install -g react-native-cli
# 適当なディレクトリで初期化
$ mkdir $HOME/Documents/react-native
$ cd $HOME/Documents/react-native
$ react-native init ProjectName

最初の方は順調だったのだが、なんかエラーが、、

checking for arm-apple-darwin-gcc... /Library/Developer/CommandLineTools/usr/bin/cc -arch armv7 -isysroot 
checking whether the C compiler works... no
xcrun: error: SDK "iphoneos" cannot be located
xcrun: error: SDK "iphoneos" cannot be located
xcrun: error: SDK "iphoneos" cannot be located

xcodeの参照先パス?が違うらしい。

#現在の状態
$ xcode-select -p
/Library/Developer/CommandLineTools
#変更してみる
$ sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer/
#結果確認
$ xcode-select -p
/Applications/Xcode.app/Contents/Developer

起動

Documents/react-native/ProjectName/ios/ProjectName.xcworkspace
を開く。

※ProjectName.xcodeprojを開くと、

 ld: library not found for -lDoubleConversion error occurs. I could build react-native run-ios

とか言われる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsでローカルファイルを軽やかに操作するスクリプト集

はじめに

こんにちは、たか(@HighHawk5)です。

最近、50GBのアクセスログファイルを解析する機会があったのですが、データサイズが大きすぎて普通のファイルシステムでは途中で処理が落ちてしまうんですね。
そこで、Node.jsの非同期処理(ストリーム)を採用したところ、なんの問題もなくサクサクと処理できたので、Node.jsのファイルシステムを使ってローカルファイルを操作できるスクリプト集を作りました。
ローカルファイルを一括で修正したり、解析したり、スクリプトを組み合わせて色々なファイル操作に活用してみてください。

今回つくったスクリプトはすべてGitHubに上げておきましたので、ダウンロードしてすぐお使いになれます。

nodeFileSystem
├── changeLines.js
├── deleteFiles.js
├── grepFiles.js
├── listUpFiles.js
├── modules
│   └── fsCustom.js
├── readme.md
├── datas
└── results

GitHub:https://github.com/nkoutaka/nodeFileSystem

以下ではそれぞれの機能を逆引き辞典風にご紹介します。

特定のディレクトリ配下にあるファイルをリストアップしたい

特定のディレクトリ配下に存在するファイルを再帰的に取得し、ファイル名(ファイルパス)のリストを作成するスクリプトです。

const fs = require('fs');

/**
 * 指定したディレクトリ配下のファイルを再帰的にリストアップする
 * @param {string} dirPath 対象ディレクトリのフルパス
 * @return {Array<string>} ファイルのフルパス
 */
const listFiles = dirPath => {
    const files = [];
    const paths = fs.readdirSync(dirPath);

    for (let name of paths) {
        try {
            const path = `${dirPath}/${name}`;
            const stat = fs.statSync(path);

            switch (true) {
                case stat.isFile():
                    files.push(path);
                    break;

                case stat.isDirectory():
                    files.push(...listFiles(path));
                    break;

                default:
            }
        } catch (err) {
            console.error('error:', e.message);
        }
    }

    return files;
};

fs.readdirSync(dirPath)ではディレクトリ直下のファイル一覧しか取得できないため、取得したデータをディレクトリと判定した場合はさらに取得処理を繰り返すことで、再帰的なファイル取得を実現しています。

使用例:現行のWEBサイトで使用している画像・CSS・JSファイルがどこにどのくらいあるか(総数・場所・ファイル名)を調べるため、実際のファイルベースでリストアップする際に使用。

特定のファイルが存在するか確かめたい

指定したパスのファイルが存在するか確認し、boolean値を返すスクリプトです。

const fs = require('fs');
/**
 * 指定したフルパスのファイルが存在するか確認する
 * @param {string} filePath 対象とするファイルのフルパス
 * @return {boolean}
 */
const isExistFile = filePath => {
    try {
        fs.statSync(filePath);
        return true
    } catch (err) {
        if (err.code === 'ENOENT') return false
    }
};

fs.statSync(filePath)は対象バスが存在しない場合にエラーを返すため、try構文でラップしてあります。

使用例:用意したファイル一覧に従って各ファイルが存在するかしないかを調査する際に使用。

特定のファイルを削除したい

指定したパスのファイルを削除するスクリプトです。
削除するパスのリストファイル(例:./data/deleteFiles.csv)を用意して、一括削除もできます。

const path = require('path');
const fs = require('fs');
const fsCustom = require('./modules/fsCustom');

//削除するファイルのパス
const targets = ['/hoge/hoge.html', '/hoge/hoge2.html', '/hoge/hoge3.html'];

//削除するパスのリストファイル
//const datas = fs.readFileSync('./data/deleteFiles.csv', 'utf8');
//const targets = datas.split(/\n/);

for (const target of targets) {
    const filePath = path.resolve(__dirname, target);
    try {
        if (fsCustom.isExistFile(filePath)) {
            fs.unlinkSync(filePath);
            console.log(`${filePath}を削除しました。`);
        }
    } catch (err) {
        console.error('error:', err.message);
    }
};

使用例:用意したファイル一覧に従って各ファイルを一括削除する際に使用。

データサイズの大きなファイルを一行ずつ取得して修正したい

特定のディレクトリ配下にあるファイルを再帰的に取得し、一行ずつ修正していくスクリプトです。
例えば、一行ずつ正規表現で文字列を修正していき、それを一つのファイルにまとめるなど。修正処理部分は適宜変更してお使いください。

const path = require('path');
const fs = require('fs');
const fsCustom = require('./modules/fsCustom');
const readline = require('readline');

//修正するファイルがあるディレクトリ
const dirPath = path.resolve(__dirname, './datas');
const files = fsCustom.listFiles(dirPath);

for (const file of files) {
    const stream = fs.createReadStream(file, 'utf8');
    const reader = readline.createInterface({ input: stream });
    reader.on('line', (data) => {
        //修正処理
        const match = data.match(/ ("[A-Z]{3,7} \/.+?) .+?" [0-9]{3} /);
        if (match && match[1]) {
            fs.appendFileSync(`./results/changeLines.csv`, `${match[1]}"\n`, (err, data) => {
                if (err) console.log(err);
                else console.log('write end');
            });
        }
    });
}

使用例:普通に開いたら落ちてしまうほど大きいサイズのファイルから必要な文字列だけを一行ずつ抽出して、新たに小さなサイズのファイルとしてまとめる際に使用。

キーワードが含まれるファイルをリストアップしたい

特定のディレクトリ配下にあるファイルを再帰的に取得し、各キーワードが含まれるファイルをリストアップするスクリプトです。

const path = require('path');
const fs = require('fs');
const fsCustom = require('./modules/fsCustom');

//検索ワードの配列
const keywords = ['/hoge/hoge.html', '/hoge/hoge2.html', '/hoge/hoge3.html']

//検索ワードのリストファイル
//const datas = fs.readFileSync('./data/grepFiles.csv', 'utf8');
//const keywords = datas.split(/\n/);

//検索対象のディレクトリ
const dirPath = path.resolve(__dirname, './');
const files = fsCustom.listFiles(dirPath);


for (let keyword of keywords) {

    const results = [keyword];

    keyword = keyword.replace(/\./g, '\\.');

    for (const file of files) {
        //正規表現で検索ワードを生成
        let regex = `("|'|\\|)(@\\{)?${keyword}.*`;
        let data = fs.readFileSync(file, 'utf8');
        let count = (data.match(new RegExp(regex, 'g')) || []).length;
        if (count) {
            results.push(file);
        }
    };
    fs.appendFileSync(`./results/grepFiles.csv`, results.join(',') + '\n', (err, data) => {
        if (err) console.log(err);
        else console.log('write end');
    });

};

使用例:現行のWEBサイトで「不要な画像ファイル」を探し出すために使用。CSS・JS・DOMファイルの中身を再帰的に検索し、画像ファイル名が全く含まれていない場合は不要と判定。

まとめ

大きなデータサイズのファイルを処理する場合はメモリリークに気を使います(JavaScriptの変数に格納できるデータ量は1GBほど)。
その際はぜひ、Node.jsの非同期処理を活用してみましょう。

GitHub:https://github.com/nkoutaka/nodeFileSystem

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む