Fastify.jsは、node.jsの最速のWebフレームワークであるだけではありません

Express.jsは、過去10年間、node.jsで最も人気のあるWebフレームワークです。これを使用したことのある人なら誰でも、複雑なExpress.jsアプリケーションを構築するのが難しい場合があることを知っています。しかし、彼らが言うように、習慣は第二の性質です。 Express.jsは放棄するのが難しい場合があります。たとえば、禁煙するのは難しいです。この無限のミドルウェアチェーンが絶対に必要なようです。何らかの理由で、理由もなくミドルウェアを作成する機能を奪うと、プロジェクトは停止します。



ついに、すべての人にとってメインのWebフレームワークの場所にふさわしい候補ができたことは喜ばしいことです。つまり、Fastify.jsではなく、もちろんNest.jsです。人気の定量的指標という点ではありますが、Express.jsからは非常に遠いです。



テーブル。npmjs.org、github.comからのパッケージ人気指標

番号。 パッケージ ダウンロード数 「星」の数
1 接続する 4 373 963 9100
2 エクスプレス 16 492 569 52,900
3 コア 844 877 31,100
nestjs 624 603 36,700
ハピ 389 530 13,200
6 固定する 216 240 18,600
7 再修正 93,665 10100
8 ポルカ 71 394 4,700




Express.jsは、node.jsWebアプリケーションの2/3以上で引き続き機能します。さらに、node.jsで最も人気のあるWebフレームワークの2/3は、Express.jsアプローチを使用しています。(Express.jsがバージョン4より前に基づいていたConnect.jsライブラリのアプローチと言った方が正確です)。



この投稿では、node.jsの主要なWebフレームワークの機能と、Fastify.jsを次のプロジェクトを開発するためのフレームワークとして選択できる異なるレベルのフレームワークにする理由について説明します。



同期ミドルウェアに基づくフレームワークへの批判



この種のコードの何が問題になっている可能性がありますか?



app.get('/', (req, res) => {
  res.send('Hello World!')
})

      
      





1.ルートを処理する関数は値を返しません。代わりに、応答(res)オブジェクトのメソッドの1つを呼び出す必要があります。このメソッドが明示的に呼び出されない場合、関数が戻った後でも、クライアントとサーバーは、各タイムアウトが期限切れになるまでサーバーの応答を待機している状態のままになります。これらは「直接的な損失」に過ぎませんが、「逸失利益」もあります。この関数が値を返さないという事実により、要求された機能、たとえば、クライアントに返された応答の検証やログ記録を単純に実装することは不可能になります。



2. Express.jsでは、組み込みのエラー処理は常に同期しています。ただし、非同期操作を呼び出さずにルートを実行することはまれです。Express.jsは産業革命以前の時代に構築されたため、非同期エラーの標準の同期エラーハンドラーは機能せず、非同期エラーは次のように処理する必要があります。



app.get('/', async (req, res, next) => {
   try {
      ...
   } catch (ex) {
      next(ex);
   }
})

      
      





またはこのように:



app.get('/', (req, res, next) => {
   doAsync().catch(next)
})

      
      





3.サービスの非同期初期化の複雑さ。たとえば、アプリケーションはデータベースを操作し、変数に参照を格納することでサービスとしてデータベースにアクセスします。Express.jsルートの初期化は常に同期しています。つまり、最初のクライアントリクエストがルートに到着し始めたとき、サービスの非同期初期化はまだ解決する時間がない可能性が高いため、非同期コードをルートに「ドラッグ」して取得する必要があります。このサービスへのリンク。もちろん、これはすべて実現可能です。しかし、それは元のコードの素朴な単純さからは程遠いものです。



app.get('/', (req, res) => {
  res.send('Hello World!')
})

      
      





4.そして最後に、大事なことを言い忘れました。ほとんどのExpress.jsアプリケーションは、次のようなものを実行します。



app.use(someFuction);
app.use(anotherFunction());
app.use((req, res, nexn) => ..., next());

app.get('/', (req, res) => {
  res.send('Hello World!')
})

      
      





アプリケーションの一部を開発するときは、コードの前に10〜20個のミドルウェアがすでに機能していることを確認できます。これにより、reqオブジェクトのあらゆる種類のプロパティがハングし、実際とまったく同じように元のリクエストを変更することもできます。アプリケーションの一部を開発した後、ミドルウェアを追加できない場合でも同じ量を追加できます。ちなみに、Express.jsのドキュメントでは、追加のプロパティをアタッチするためにres.localsオブジェクトがあいまいに推奨されています。



//   Express.js
app.use(function (req, res, next) {
  res.locals.user = req.user
  res.locals.authenticated = !req.user.anonymous
  next()
})

      
      





Express.jsの欠点を克服するための歴史的な試み



当然のことながら、Express.jsとConnect.jsの主な作者であるTJ Holowaychukはプロジェクトを離れ、新しいKoa.jsフレームワークの開発を開始しました。Koa.jsはExpress.jsに非同期を追加します。たとえば、このコードは、各ルートのコードで非同期エラーをキャッチする必要をなくし、ハンドラーを1つのミドルウェアに配置します。



app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    // will only respond with JSON
    ctx.status = err.statusCode || err.status || 500;
    ctx.body = {
      message: err.message
    };
  }
})

      
      





Koa.jsの初期のバージョンは、非同期呼び出しを処理するジェネレーターを導入することを目的としていました。



// from http://blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators/
var request = Q.denodeify(require('request'));
 
// Example of calling library code that returns a promise
function doHttpRequest(url) {
    return request(url).then(function(resultParams) {
        // Extract just the response object
        return resultParams[];
    });
}

app.use(function *() {
    // Example with a return value
    var response = yield doHttpRequest('http://example.com/');
    this.body = "Response length is " + response.body.length;
});

      
      





async / awaitの導入により、Koa.jsのこの部分の有用性が否定され、フレームワークのドキュメントにもそのような例はありません。



Express.jsとほぼ同じ年齢-Hapi.jsフレームワーク。 Hapi.jsのコントローラーは、Express.jsからのステップアップである値を既に返します。 Hapi.jsプロジェクトのコンポーネントであるExpress.jsに匹敵する人気を得ていません-npmjs.orgから3,388,762のダウンロードがあり、現在バックエンドとフロントエンドの両方で使用されているJoiライブラリは大成功を収めています。着信オブジェクトの検証は特別な場合ではありませんが、すべてのアプリケーションに必要な属性であることに気づきます。Hapi.jsでの検証は、フレームワークの一部として、およびルートの定義のパラメーターとして含まれています。



server.route({
    method: 'GET',
    path: '/hello/{name}',
    handler: function (request, h) {
        return `Hello ${request.params.name}!`;
    },
    options: {
        validate: {
            params: Joi.object({
                name: Joi.string().min(3).max(10)
            })
        }
    }
});

      
      





現在、Joiライブラリはスタンドアロンプ​​ロジェクトです。



オブジェクト検証スキームを定義した場合は、オブジェクト自体を定義しました。データ検証スキーマの変更によってドキュメントが変更される自己文書化ルートを作成する余地はほとんどないため、ドキュメントは常にコードと一致します。



これまでのところ、APIドキュメントの最良のソリューションの1つは、swagger / openAPIです。swagger / openAPIの要件を考慮したスキーマ、説明を検証とドキュメントの生成の両方に使用できると非常に便利です。



Fastify.js



Webフレームワークを選択するときに私にとって不可欠と思われる要件を要約します。



  1. ( ).
  2. .
  3. .
  4. / .
  5. .
  6. .


これらの点はすべて、私が現在いくつかのプロジェクトに取り組んでいるNest.jsに対応しています。Nest.jsの機能は、デコレータの幅広い使用です。これは、技術要件で標準JavaScriptの使用が指定されている場合に制限となる場合があります(ご存知のとおり、JavaScriptでのデコレータの標準化により、この状況はいくつか停滞します。数年前、そしてそれはすぐにその解決策を見つけることができないようです)...



したがって、代わりにFastify.jsフレームワークを使用することもできます。このフレームワークの機能を分析します。



Fastify.jsは、Express.js開発者に馴染みのあるサーバー応答を生成するスタイルと、関数の戻り値の形でより有望なスタイルの両方をサポートし、他の応答パラメーター(ステータス、ヘッダー)を柔軟に操作する機能を残します。



// Require the framework and instantiate it
const fastify = require('fastify')({
  logger: true
})

// Declare a route
fastify.get('/', (request, reply) => {
  reply.send({ hello: 'world' })
})

// Run the server!
fastify.listen(3000, (err, address) => {
  if (err) throw err
  // Server is now listening on ${address}
})

      
      





const fastify = require('fastify')({
  logger: true
})

fastify.get('/',  (request, reply) => {
  reply.type('application/json').code(200)
  return { hello: 'world' }
})

fastify.listen(3000, (err, address) => {
  if (err) throw err
  // Server is now listening on ${address}
})

      
      





エラー処理は、組み込み(箱から出して)およびカスタムにすることができます。



const createError = require('fastify-error');
const CustomError = createError('403_ERROR', 'Message: ', 403);

function raiseAsyncError() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new CustomError('Async Error')), 5000);
  });
}

async function routes(fastify) {
  fastify.get('/sync-error', async () => {
    if (true) {
      throw new CustomError('Sync Error');
    }
    return { hello: 'world' };
  });

  fastify.get('/async-error', async () => {
    await raiseAsyncError();
    return { hello: 'world' };
  });
}

      
      





同期と非同期の両方のオプションは、組み込みのエラーハンドラーによって同じ方法で処理されます。もちろん、組み込み機能は常にほとんどありません。エラーハンドラをカスタマイズしましょう:



fastify.setErrorHandler((error, request, reply) => {
  console.log(error);
  reply.status(error.status || 500).send(error);
});

  fastify.get('/custom-error', () => {
    if (true) {
      throw { status: 419, data: { a: 1, b: 2} };
    }
    return { hello: 'world' };
  });

      
      





コードのこの部分は単純化されています(エラーはリテラルをスローします)。同様に、カスタムエラーをスローできます。(カスタムのシリアル化可能なエラーの定義は別のトピックであるため、例は提供されていません)。



検証のために、Fastify.jsは、swagger / openAPIインターフェースを実装するAjv.jsライブラリを使用します。この事実により、Fastify.jsをswagger / openAPIと統合し、APIを自己文書化することが可能になります。



デフォルトでは、検証は最も厳密ではありません(フィールドはオプションであり、スキーマにないフィールドは許可されます)。検証を厳密にするには、Ajv構成と検証スキームでパラメーターを定義する必要があります。



const fastify = require('fastify')({
  logger: true,
  ajv: {
    customOptions: {
      removeAdditional: false,
      useDefaults: true,
      coerceTypes: true,
      allErrors: true,
      strictTypes: true,
      nullable: true,
      strictRequired: true,
    },
    plugins: [],
  },
});
  const opts = {
    httpStatus: 201,
    schema: {
      description: 'post some data',
      tags: ['test'],
      summary: 'qwerty',
      additionalProperties: false,
      body: {
        additionalProperties: false,
        type: 'object',
        required: ['someKey'],
        properties: {
          someKey: { type: 'string' },
          someOtherKey: { type: 'number', minimum: 10 },
        },
      },
      response: {
        200: {
          type: 'object',
          additionalProperties: false,
          required: ['hello'],
          properties: {
            value: { type: 'string' },
            otherValue: { type: 'boolean' },
            hello: { type: 'string' },
          },
        },
        201: {
          type: 'object',
          additionalProperties: false,
          required: ['hello-test'],
          properties: {
            value: { type: 'string' },
            otherValue: { type: 'boolean' },
            'hello-test': { type: 'string' },
          },
        },
      },
    },
  };

  fastify.post('/test', opts, async (req, res) => {
    res.status(201);
    return { hello: 'world' };
  });
}

      
      





着信オブジェクトのスキーマはすでに定義されているため、swagger / openAPIドキュメントの生成は、プラグインのインストールに帰着します。



fastify.register(require('fastify-swagger'), {
  routePrefix: '/api-doc',
  swagger: {
    info: {
      title: 'Test swagger',
      description: 'testing the fastify swagger api',
      version: '0.1.0',
    },
    securityDefinitions: {
      apiKey: {
        type: 'apiKey',
        name: 'apiKey',
        in: 'header',
      },
    },
    host: 'localhost:3000',
    schemes: ['http'],
    consumes: ['application/json'],
    produces: ['application/json'],
  },
  hideUntagged: true,
  exposeRoute: true,
});

      
      





応答の検証も可能です。これを行うには、プラグインをインストールする必要があります。



fastify.register(require('fastify-response-validation'));

      
      





検証は十分に柔軟です。たとえば、各ステータスの応答は、独自の検証スキームに従ってチェックされます。



記事の執筆に関連するコードは ここにあります



情報の追加ソース



1. blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators

2. habr.com/ru/company/dataart/blog/312638



apapacy@gmail.com

月4 2021年



All Articles