モノリシックなサービスにBFFを導入して生産性を爆上げさせた話
サイトのviewを表現する方法として、サーバーサイド言語のHTMLテンプレートエンジンでviewを実現するモノリシックな構造を採用していました。
session情報などのクライアント側からサーバーサイド側へのデータのやり取りをあまり意識しなくて済む点ではモノリシックな構造は有効的であると言えます。
しかし、モノリシックな構造でやり続けることで以下の課題にぶち当たり、改善策を検討しました。
1. ランディングページのようなフロントだけで済むものも、バックエンドに依存してしまっていた
2. バックエンド側のビジネスロジックの老朽化に伴う、システムリプレイスもクライアント側の影響を考慮して安易に手を付けることができない
3. シームレスな画面遷移を提供したい場合JSで実装するのがマストになってくるが、 テンプレートエンジンの関係上SEOと操作性どっちかを犠牲にする事があった
4. HTMLテンプレートエンジンに段階的にVue.jsを導入したが、開発体験が悪い等使いこなしていくには限界になっていた
そこで、フロントエンド用にBFFを取り入れ、バックエンドのサーバーとフロントエンドのサーバーを切り離すことでこれらの課題の解決を試みました。
今回は、その内容についてと、私的に一筋縄でいかなかったところのご紹介をさせていただければと思います。
技術選定
BFFを構築するのに Nuxt.jsのSSRモードを利用しました。
Nuxt.js を採用した理由は以下になります
■Vue.jsはHTMLの部分に<template> 構文を使うことができる
⇒ React等で使われている、JSXの学習コストが高い
■日本のフロントエンド界隈ではReactよりもVueのほうが参考文献が多い
⇒ 個人で使うならReactのほうが良いが、組織で使う場合は学習コストが低いほうが良い
■Nuxt.jsはルーティングも含めある程度作り込まれている
⇒Vue.jsでフルスクラッチで作り込むとオレオレ実装ができがちなので、最初からある程度作り込まれているNuxt.jsを採用
JSフレームワークの採用基準は色々あると思いますが、今回社内に導入するにあたって "私以外はほとんどがJSフレームワーク未経験"
という前提条件があったので、未経験の人が低コストで改修ができるようになるというところにフォーカスを当てて採用しました。
構築方法
BFFを実現するために、以下の構成で環境を構築しました
Node.jsのExpressフレームワークのmiddlewareとしてビルドしたNuxt.jsを読み込ませるという公式でも紹介しているやり方で構築しています。
Nuxt.jsのdefaultはconnectを使っているんですが、Express用の実装が使えるので拡張性が上がる点から、connectではなくExpressでの実装を採用しました。
※Nuxt.js本体はconnectになっているので注意が必要になります。
個人的に頑張ったところ
ここからは、私が環境を構築していく中で純正の機能では足りない部分など自分的に一筋縄でいかなかった部分を紹介させて貰えればと思います。
1. エラーログ
バックエンド側と切り離されたことで、BFFサーバーが担うエラーログはバックエンド側と同じようにサーバー側に保存できる必要がありました。
しかし、JavaScriptのログモジュールはNode.js限定でしか実行できないものがほとんどなものなので、よくあるフレームワークのように雑にlogHandlerをmiddlewareに突っ込むという手法をとることができず、そこでこんなやり方で実現しました。
ログモジュールは社内でも実績があり、汎用的に使われているに使われているlog4jsを使用しました。
■アクセスログ
⇒ log4jsの中にあるconnectLoggerでmiddlewareを定義してあげるとアクセスログを生成することができます。
const app: Express.Express = Express()
export const accessMiddleware = log4js.connectLogger(
log4js.getLogger('access'),
{}
)
app.use(accessMiddleware)
■サーバーエラーハンドリング
⇒ Nuxt.js(connect)側で発生した例外はExpress側で拾うことができません。
それでは肝心なログを取り逃してしまうのでNuxt.js側で例外が発生した際にExpressのエラーハンドラーを実行する形で解決しました。
Nuxt.js で発生した例外をフックするためには、以下のようなModuleを作って上げることで実現できます。
const nuxtLog4js: Module<Options> = function(moduleOptions: Options) {
this.nuxt.hook('render:errorMiddleware', (app: Server) =>
app.use(errorHandler)
)
this.addPlugin({
src: path.resolve(__dirname, 'plugin.server.ts'),
options: moduleOptions,
mode: 'server'
})
this.addPlugin({
src: path.resolve(__dirname, 'plugin.client.ts'),
options: moduleOptions,
mode: 'client'
})
}
nuxt.hook('render:errorMiddleware')でNuxtの例外が発生した際にフックすることができるのでその中でExpressのエラーハンドラーを定義します。
■サーバーログ
⇒ Nuxt.jsではdefaultのconsole.logを使うことができますが、今回はログをシステムの仕様の都合でファイル出力する必要があったため、以下のようなplugin.server.ts を作成しサーバーサイド限定で読み込む形にしています。
declare module '@nuxt/types' {
interface Context {
$logger: Logger
}
}
const plugin: Plugin = (context) => {
context.$logger = logger
}
export default plugin
これによって Nuxt.js内でasyncData(context){context.$logger}といった形でlog4jsのロガーを呼び出すようにできるようになりました。
■クライアントログ
⇒サーバーサイドログでは log4jsを利用しましたが、log4jsはクライアントサイドで利用することができません。解決方法としてはJavaScript用のログ機能を用意しているマネージドログサービスを利用するやり方が一般的ですがまだ導入していないため今回は以下のようなやり方で実現しました。
interface clientLogger {
info: (message: any) => void
error: (message: any) => void
warn: (message: string) => void
debug: (message: string) => void
}
declare module 'vue/types/vue' {
interface Vue {
$logger: clientLogger
}
}
Vue.prototype.$logger = {
info: (message: string) =>
axios.post('/api/logger/info', {
message
}),
error: (message: string) =>
axios.post('/api/logger/error', {
message
}),
warn: (message: string) =>
axios.post('/api/logger/warn', {
message
}),
debug: (message: string) =>
axios.post('/api/logger/debug', {
message
})
}
log4jsのログを実行するExpressのmiddlewareを定義しておき、それを呼び出す形でサーバーのログとして保存するという方法です。
暫定的ではありますが、これでクライアントサイドのログを比較的に簡単にサーバーのログとして収集することができます。
※Nuxt.jsは気軽にエンドポイントを立てることができるもの良いですよね
2. TypeScriptの導入
今回プロジェクトを作るに辺り、TypeScriptを導入しました。
サーバーサイドがscalaが採用されていることもあり、社内的にも型がある方が好まれる傾向にあります。
■導入手順
基本的には公式が用意している手順でnuxt create appを使って導入しました。
しかし、TypeScriptはvue.jsデフォルトのoptionsAPIとの相性が悪く、APIは検討する必要がありました。
他のプロジェクトではvue-property-decoratorを使ってclassAPIで実装しています。
もうすぐ公式リリースされるであろうVue.js 3 はclassAPIがリジェクトされcompostion APIが採用されることになっています。しかし、compostionAPIはNuxt.jsのAPIを全てサポートしていないためAsyncData等を使うには難しいです。
そこで、今回は以下の形でプロジェクトでキメラになる形を採用しcompostionAPIが正式リリースされたらリファクタリングする形を取りました。
● Nuxt.jsのAPIを使うコンポーネント ⇒ optionsAPI
● Nuxt.jsのAPIを使わないコンポーネント ⇒ compostionAPI
3. 今までバックエンドサーバーで処理していたもの
例えばプロモーション情報をCookieに付与する処理など、今までバックエンドサーバーで実装していたものは一部実装し直す必要がありました。
今回BFFとしてExpressを採用しているため、基本的にExpressのmiddlewareを作ることで解決することができました。
Expressであれば既存ですでにmiddlewareがモジュールであったり、すでにやっている人が多いので便利ですね。
また、Expressにいわゆるサーバー処理を書くことで、Express(BFF)なのかNuxt.js(client)なのかの区別が付きやすくなる点が良いと思います。
実際にリリースしてみての変化
1. サイトのパフォーマンスが大幅に向上した
PageSpeed Insights での実測値が同じコンテンツの表示で2倍強になりました。
まだまだ改善の余地はありますが、スタートラインに立つことができました。
2. 開発の効率化
従来 : フロントエンドがHTMLのテンプレートを作ってからバックエンドがそれをテンプレートエンジンに当てはめるため同時進行が難しい
現在 : バックエンドのAPIに合わせて、テンプレートを作ると同時に実装ができるようになるので同時進行が可能に
3. コンポーネント指向を意識するように
今まではペライチでページを作ってそれに対して、HTMLとCSSを書くという作業のフローだったのが、コンポーネントを意識することによって「これはデザインからパーツの共通化ができれば効率が良さそう」といったコンポーネント指向の穏健を受けることのできる考え方がチームでできるようになった。
さいごに
タイトルは少しオーバーなところもありますが、
今回は、BFFを導入した流れについて紹介させていただきました。
これから、BFFを導入検討の方に参考になれば幸いです。
今回のBFFは導入は、私自身で課題提示から検証・実現まですべて一人でやれたのは大きな経験になったなと思います。
私はメインではサーバーサイドエンジニアなのですが、最近はfirebaseのようにバックエンドを実装しないパターンも増えてきているので、職種の枠にとらわれずに幅広い領域で技術をキャッチアップしていって実際の行動していくことってすごく重要ですよね。