🔬

レガシーおじさん、SPAを始めてみた。そして限界を知る

2020/10/19に公開
4

はじめに

最近、Webの記事を見てるとReactだVue.jsばかりが上がっていてJSPやERBの話をしてる人は誰もいません。jQueryの記事ももちろん見ない。
つまり、Webだけ見る限りではほとんどの人がSPAを使ってるように見えます。

私はWeb界隈には居るもののどちらかというとバックエンド寄り、もっというとそもそもWebとか関係ない領域を見る事が多いので、ちょっとキャッチアップを兼ねていくつかの個人プロダクトにVue.jsを採用してみました。
jQueryくらいで頭が止まってたので。サーバサイドもマイクロサービスでAPI化が進んでるのでフロントもそれに合った技術を選ばないとですしね。

というわけで、今回はその中で得た知見というか、従来型のサーバサイドでのWeb開発をしていた人の視点でVue.jsをキャッチアップする流れで書いていきたいと思います。

まあ最終的な結論は正直「これすごく便利だけど、みんな本当に本番環境で使ってるの???」なんですけれど。。。

なお、相変わらずこの分野は文字通り詳しくないので間違ってたりより適切な方法があればご指摘ください。

CRUDアプリの作り方。あるいは普通のWebサイトの作り方

たぶん、従来のWeb開発者が一番最初に躓くのは単純なCRUDアプリの作り方です。
SPAの初心者向けチュートリアルの多くはページ遷移を伴わないものが多いです。これは従来のWebアプリと違い一つの画面で多くのインタラクションが出来るとかVue Routerを意識しなくて良いとかそういうった理由だとは思うんですが、ちょっと困りました。

というのも頭がやはりRails的なlist/ceate/edit/deleteに慣れており、あらゆるWebサイトはそれの変形で表現するのを当然と思ってるからですね。おじさんは「URLを変更したい」のです。SPAらしくないシステムの作り方が入門編に載ってないのは道理ですが、作りたいシステムは「普通の今まで通りのWebサイト」なので、困ります。v-modelの使い方とかv-ifの使い方とか大事だけどそんなのは後で良い。

というわけで、基本的なことは下記の記事に書きました。

ちょっと調べれば分かる内容ですが一番最初のショートカットにはなるかと。

あと、元記事を書いた時点ではあまり意識をしてなかったのですが 「SPAにはデータ読み込み時のユーザへのフィードバックがデフォルトでは無い」 ということに注意してください。
例えば全画面を描画しなおすのではればWebの読み込み中である事がブラウザで確認できますが、Vue.jsのようなSPAでは当然それを非同期で暗黙的にやるのでユーザには読み込み中であることがわかりません。
それが良い場合もたくさんあるのですが、何らかのローディング表現を入れないと困る事も多いです。私はvue-loading-overlayを使っています。導入もシンプルでUIもわかりやすいので個人的にはお気に入り。

Vue.jsの基本的な要素を押さえる

とりあえずCRUDページを作る事が出来たので、次の「認証機能の実装」に進む前に開発を続けるにあたって押さえておきたいポイントを書いていきます。

ただ、Vueコンポーネントの作り方とかv-modelの使い方とかそういうのはいっぱいあると思うので書きません。説明できるほど知らないというのもあるけど、やりたい事をGoogle先生に聞きながらやれば大体たどりけるでしょうし。

どちらかというと「これってどうするの?」っていう設計とか思想に近い部分を書いていきます。

Vue.jsのディレクトリ構造

ディレクトリ構成です。やはり「どこに何を置くべきか?」は悩みますからね。
ざっくり調べてみた限りだとこれ標準がありません。残念です。Webの記事とかだと人によって書き方が違うんですよね。世代というか時代によるものもあるかと。
一応、vue-cliが生成するディレクトリ構造をベースとするのが今のところ良いと思っています。

babel.config.js
package.json
public/
└── index.html
src/
├── App.vue
├── assets
├── components
    └── MyCompornent.vue
├── main.js
├── modules
│   └── auth.js
├── router
│   └── index.js
├── store
│   ├── index.js
│   └── modules
│       └── user.js
└── views
    ├── Home.vue
    ├── NotFound.vue
    └── Login.vue

大体こんな感じ。src/views/配下に各画面にあたるVueテンプレートを置く。それ以外のパーツとして作ったVueコンポーネントはsrc/components配下に置く。
src/store/は後で話すVuex向けのディレクトリで名前空間を分けたいものはsrc/store/modules/配下に置く。
悩んだのがVueコンポーネントじゃないJSライブラリの置き場所だけど、特に共通見解がなさそうだったのでsrc/modules以下に置いてる。なんかこの辺は従うべき標準があれば知りたいですね。

Vue Router

Vue.jsを使っていくうえで重要になるのがVue Router。これは古くはStrutsのstruts-config.xmlでありRailsのconfig/routes.rbです。特に目新しいものではないですね。大丈夫怖くない。
SinataraやJAX-RSのような個別に書く仕組みではなくルーティングはconfigで中央管理する方式とだけ覚えれば十分かと。

なので認証の制御とかも基本的にはVue Routerでやる事になります。各ページにルーティングする前にリクエストをフック出来るrouter.beforeEach:valueによるパスパラメータの指定の仕方は覚えて置いた方がいいですね。

Vuex。サーバサイドとは違う永続化とセッション

アプリを書いてると当然状態を保存しないといけないのですが、従来のWebアプリではURLやCookieを使ったHTTPセッションをベースにRequest Scope, Flash Scope, Application Scopeなんかを使いますがSPA界隈ではこのあたりを永続化と呼びます。

永続化というとDB書き込みをついイメージしてしまいますが純粋にクライアント側の処理です。
そもそもSPAは遷移しなければ状態はインメモリにあり続けます。dataとかに格納しておけば良いのです。
ただし、Vueコンポーネントの切替え (というか再作成)で状態を初期化してるのでVueコンポーネントを超えて別のコンポーネントから共通でアクセスしたりする場合にどこかしらにシリアライズしておく必要があるので永続化と呼ぶわけですね。

Fluxパターン? 単一方向の更新?

Vue.jsでは永続化にはVuexを使います。VuexはReduxの流れをくむFluxパターンに基づいた永続仮想で単一方向の更新を原則としており(ry なのですが正直細かい事はあまり気にせず単なる永続化の抽象レイヤーという事で良い気が今のところしています。

たぶん、複数の同じデータのコピーを様々なViewが持つと整合性管理が大変だからStoreでデータを中央管理して各Viewなどへのデータの反映はStoreにオブザーバーパターンとかを適用することでリアクティブにやろう、という話なだけの気がする。あってるとするとMVVMだし、まあそう作るんじゃないかな的な。

実装的に重要なのはdispatchとか少し特殊な呼び出しが必要になってきて記述性が悪いこと。今のところ抽象化の観点でもビジネスロジックを持つJSのクラスとかモジュールを作って直接的には各コンポーネントからstoreが見えない形で実装してるけどベストプラクティスかは謎。教えて偉い人!

永続化の方式

Vuex単体ではインメモリに保持してるだけなのでF5などでブラウザを更新すると状態が消えます。これだと困るということでやるのが永続化ですね。
Vuex単体で永続化はできないのでVuex-persistanceというライブラリを追加でインストールします。
永続化の方式は以下の三つ。

  • LocalStorage
  • SessionStorage
  • Cookies

これはその名前の通りの場所に保存していてデフォルトはLocalStorageです。つまり、ブラウザを閉じても永続化されます。Cookieもまんまかな。有効期限が指定できるのがメリット? でもサーバサイド連携がいる気がしますね。ちょっと選ぶ理由は少ないかも。
そして、曲者がSessionStorageです。これも実は名前のままですが 「HTTPセッションではなくWindow.sessionStorageというブラウザ機能」 です。ブラウザ閉じるまでの寿命となります。サーバサイドに慣れた人だとLocalStorageはブラウザに保存するクライアントストレージ、SessionStorageはサーバ側に保存するサーバストレージと誤解しがちなので注意してください。

それぞれの違いはこちらの記事が非常に分かりやすくまとめられています。

モジュールの分割。ビジネスロジックを切り出そう

Vue.jsのチュートリアルとか見てても何故かみんな 「ビジネスロジックをすべてVueコンポーネントに書いたままにしてる」 んですよね。
まあ、紙面の都合とかSPAに入門してる人はnode.jsに精通してるから自明な事は省いてるとかVue.jsの思想的に可能な限りコンポーネントにビジネスロジックを直接書くという思想があるとか、いろいろ想像は出来るのですが特に答えはなく。
そもそも、複数のコンポーネントにわたって各処理とか溢れてますからね。ユーティリティ的なものからビジネスロジック的なものまで。

この辺はJSに慣れてる人からは自明でしょうけど、JavaやRubyでサーバサイドやってた人間としてはそもそもJSのモジュール分割ってどうやるの? そんなの昔無かったよ? 的な感じだったので書いておきます。

なお、前述のとおりsrc/modules配下に今のところJSのライブラリ系は置いています。

ステートレスなロジックはモジュールとして切り出す

主に状態のないユーティリティ的なものを切り出す場合。これはシンプルにモジュールとして公開します。

import firebase from "firebase/app";
import "firebase/auth";
import store from "@/store";

export default {
  init() {..},
  loginWithTwitter(callback) {
    this.login(new firebase.auth.TwitterAuthProvider(), callback);
  },
  login(provider, callback) {..},
  user() { return store.state.user;},
};

使う時はこんな感じ。importで名前をモジュールにつけて利用できるので便利ですね。ES6以降はやっぱ便利だなぁ。<script>を無理やり埋め込んだりrequireとかもういらんのや。。。

<script>
import Auth from "@/modules/auth";
export default {
  name: "Login",
  methods: {
    signInWithTwitter: function () {
      Auth.loginWithTwitter(() => self.$router.push("home"));
    },
  },
};
</script>

ステートフルなロジックはクラスとして切り出す

ステートフルなビジネスロジック、つまりテンプレートの無いVueコンポーネントみたいなのを作りたい時は単なるモジュールでは状態が作れないのでES6以降には含まれているクラスを使います。
Vueコンポーネントにmix-inしても良いのだけど、名前空間が衝突するのが嫌なのでこの辺は好みもあるかも。

import store from "@/store";

export default class {
  constructor() {
    this.fistName = "";
    this.lastName = "";
    this.age = 0;

    //
    // Method Define
    //
    this.load = () => {...};
    this.save = (task) => {...};
  }

  get name() {
    return this.lastName + " " + this.firstName;
  }
  set name(value) {
    this.lastName = value.split(" ")[0];
    this.firstName = value.split(" ")[1];
  }
}

使う時はこんな感じ。

<script>
import User from "@/modules/user";
export default {
  name: "Login",
  data() {
    return {
      user: new User(),
    };
  },
  methods: {
    login: function (response) {
      this.user.name = response.data.fullname
    },
  },
};
</script>

プロトタイプ型のオブジェクト指向も良いものではあるのですが、やっぱりクラスの方が楽なことも多いですよね。
プロパティ構文を使うことでテンプレート側での記述も含めてすっきりします。ただ、注意点として 「JSのthis問題」 です。ご存じの通りJSのthisはコンテキストに依存して参照先が変わります。
なのでクラス内でフィールドを参照してるつもりが違う値になっていたなんていう事がメソッド構文で初期化した場合には頻出するのです。

対策としてはアロー構文を使ってthisを束縛してしまいます。そのためコンストラクタ側でメソッドの宣言相当のことを記述する必要があるのですが、これは後方互換性のためとはいえちょっとイケてないですよね。。。

API呼び出しにはaxiosを使う

Vue.jsを利用しているのにAPIコールだけにjQueryを使うのはちょっと嫌ですよね。XMLHttpRequestはさすがに面倒ですし。
JSは最近はどんどん進化しているのでいわゆるバニラJSでjQueryでやっていたCSSセレクタや良い感じのAPI呼び出しは可能です。API呼び出しの場合はFetch APIを使う事が出来ます。
ただ、これ微妙にいろいろ冗長なので素直にaxiosを使うべきです。
一応両方ためしてみましたが、Fetch APIに比べてaxiosの方がコードが格段にすっきりするので絶対にバニラJSでやりたいという強いこだわりがなければaxiosがお勧めです。

アクセス制御とログイン機能を実装する

CRUDページが出来た所で認証を実装していきましょう。SPAにおける認証は従来型のWebアプリケーションとは大きく違います。

従来はサーバサイドの認証だけ作ればHTTPセッションを利用してテンプレートも認証していたので気にする必要はなかったのですが、SPAではAPIサーバとクライアントで切り離されているため、両方ともそれぞれに認証/認可が必要です。Vue.js自体をサーバサイドのテンプレートエンジンに乗せてやる方法もありますが、移行期の苦肉の策以外ではメリットもない気がします。

トークン認証を実装する

というわけでSPAでは一般的にはトークン認証を使います。
OAuthやOIDCが有名ですけど、JWTを使って認証する今風のアレです。

作り方は下記の通り. Vue.js側としてはVue Routerでアクセス制御をしてVuexでトークン情報を永続化するという感じです。

トークン認証では認証サーバが必要になりますがFireabase認証/GCP Identity PlatformとかAuth0を使うのが簡単で良いですね。認証機能はセキュリティホールの塊なので原則自分で作らないというのが重要です。作るならせめてFIDO2のWebAuthnを採用することを考えるべきです。

ステートレス認証? クライアントにトークンって保存して良いんだっけ?

先ほどVuexはすべてクライアントストレージだと言いました。とするとトークンをVuexに保存すると認証情報がクライアントのみに存在するのでセキュリティ的に危険では? と思った方もいると思います。
はい。その懸念は正しいです。

なので、JWTなどによるToken認証では通常はリフレッシュトークンとアクセストークンの2つを用意します。

アクセストークンは短寿命(数分から1時間程度)のトークンでこれはステートレスに実装します。これによりサーバサイドでは公開鍵を使って認証が出来きるのでDB通信等がいらず負荷を大幅に下げることができます。一方でこれでは強制ログアウトもできませんし、クライアントのトークンが何らかの理由で流出すると全員の公開鍵を変えるとかしか手がありません。

そのため認証サーバは比較的長寿命でステートフルのリフレッシュトークンを作りこれで認証サーバに問い合わせることでアクセストークンを発行しています。リフレッシュトークンはステートフルなのでサーバ側で破棄出来ますから流出時のリスクも少なくクライアントに保存しても問題ありません。

この仕組みによって、認証サーバへの負荷を下げつつクライアントサイドにトークンを保存してもセキュリティリスクが高まらないようにしてるわけです。

詳しくは下記の記事にまとめておいたので気になる人は読んでみてください。

Fireabse SDKなどはリフレッシュトークンによるアクセストークンの更新を透過的にやってるのであたかもアクセストークンしかもって無いようにも見えますが、そんなことは無いので大丈夫です。

デプロイ編。解決してなかったSEO。そしてSSRへの道

さて、CRUDと認証ができれば後は好きにアプリを作るだけですよね。
出来上がれば最後にサーバサイドにデプロイする必要があるわけです。

どこにデプロイするか?

デプロイの仕方は大きく分けて2つあります。

  • アプリケーションの一部として同一ドメインに置く
  • APIサーバとは別の静的ホスティングサービスに置く

前者の方法が運用的には簡単です。一方でリバースプロキシやロードバランサで振り分けてない限りはアプリケーションサーバ上で画像やJS、HTMLを処理する事になるのでコスパ的には少しもったいない構成になっており中規模以上のサービスであればあまりやらない気がします。
モジュールは別なのでビルド時に上手くミックスるというのをCIサーバでやればいいですね。

後者はAPIサーバとは別に静的ファイルをCDNや各種クラウドのオブジェクトストレージに配置する方法です。
CDNという高速で低コストかつほぼ落ちないサーバにデプロイできれば一番良いですよね。私も個人サービス自体は大規模では無いのですがキャッチアップを兼ねている特性上この方式を基本的には選んでいます。

GCS + Cloud Load balancerを使っていて若干ロードバランサの最低コストがかかるのが痛いものの、後述するSEO以外では大きな不満は無い感じです。そこが大問題だけど。。。

CORS

CORS (Cross-Origin Resource Sharing)の設定しないと当たり前だけどエラーになります。設定方法はAPIサーバ側によって違いますがQuarkusならこちら。JSON-Pとか言ってる人は今すぐググるのです!

直接URLを指定すると404エラーになる問題

開発中にyarn serveとかしてる時には問題ないのですが、いざ本番環境にデプロイしてみるとF5で更新した時やブラウザから直接リンクを指定したときに404エラーが出るという不可解な現象に出くわします。

まあ、これは当然でVue.jsのURLというのはVue Routerにより生成されたものであり実際のHTMLや画像といった静的ファイルがあるわけじゃないので404エラーになるわけですね。
なので必ずVue.jsのindex.htmlを経由させる必要があるわけですがたいていのアプリケーションサーバやホスティングサービスには「すべてのリクエストのハンドリングを渡す」なんて機能はありません。

なので、404エラーのページを指定する代わりにindex.htmlを指定してやります。これはアプリケーションサーバの時も同様です。詳しくは以下にまとめておきました。

つづいて404の管理をサーバサイドからVue.jsに委譲したわけですから、今度は404をVue.jsで実装してやる必要あります。以下のようにVue Routerに設定しましょう

const routes = [
  {
    path: "/",
    name: "Login",
    component: Login,
  },
  {
    path: "/user/:id",
    name: "User",
    component: User,
    meta: { requiresAuth: true },
  },
  {
    path: '*',
    name: "error404",
    component: NotFound,
  }
];

*をパスに指定する事で指定していないすべてのリクエストをフックすることができます。これで 「見た目上は」 良い感じに解決することができました。

解決してなかったSEO

SPAの課題としてまず最初に思いつくのはSEOです。
SPAが登場した当時は便利そうだけど原理的にサーチエンジンのクローラと相性悪いから社内システムにしか使えないよね、感がありました。
特に新しい課題でもなくWeb 2.0とか言ってAJAXが隆盛してからJS中心のページのSEOはずっと課題でしたけどね。

最近は冒頭にも書いた通り、Web界隈では猫も杓子もSPAの話題ばかりですし、事実GoogleのクローラはJSを解釈するという話も聞いていたのでSEO問題はSPAにおいては過去の遺物だと思ってましたがそんなことはなかったようです。

検索エンジンのクローリング。これは解決

多少制限があるとも聞きますがGoogleさんは基本的にはJSを解釈したうえでクローリングしてくれるようです。なのでSPAで書いたから検索にそもそも引っかからないという時代では確かになくなっているようです。
これは大きな前進ですね!

Persistence URL。これは解決

SPAに限らずこれは実装が微妙な従来型のWebアプリもそうですがPersistence URLが無いケース。つまり、ブックマークや外部ページからの参照といった直リンができない事です。
初期のSPAはURLが書き換わらなかったのでこの問題が発生していました。現在は多くのFWが対応しておりVue.jsも問題なくVue RouterよりURLも変更されますし直リンも出来るので大丈夫です。

Twitterカード(OGPタグ)の動的生成問題

SEOにおいても重要であるSNSに関して、両雄であるFacebookとTwitterですが実はTwitterカードのためのクローリングにJSの実行をサポートしていません。
TwitterカードというのはTwitterやFacebook、あるいははてなブログなんかもそうですがそういったSNS系にリンクを貼った時にサムネイルが表示有れるあれです。こういうの。

これが適切に指定されてないと単なるリンクになったり謎のサムネイルを貼られたりでSEOというかユーザへの請求力に大きな差が出てしまいます。
固定のものを出すことはまあ出来るのですが、たとえばブログやレシピ投稿サイトのようなページ毎にタイトルやサムネイルを変えるべきものだとこれはちょっと微妙です。

この問題を解決するには通常のブラウザからのアクセスとTwitter等のクローラによるリクエストに対するレスポンスをサーバ側で出しわける、というものです。下記のサイトが詳しいです。

嘘つきのHTTPステータス、ソフト404問題

「直接URLを指定すると404エラーになる問題」の解決策として404の時にindex.htmlに振り分ければ良いよという話をしました。
これは解決した風なことを言いましたが、実は解決してません。

というのもこの状態ではHTTPステータス (200,30x, 40x, 50xとか)が全く信頼できないことです。
今回のような方法で作ったブログなどのサイトにパーマネントリンクでアクセスするとコンテンツは意図通りに見れますがそのHTTPステータスは404です。
また、Vue Routerで存在しないリンクへアクセスしたときにNot Foundページに連携して画面上は404と出せますがHTTPステータスは200です。このような問題をソフト404問題と呼ぶそうです。

残念ながらこの2つの問題はブラウザの使用上クライアントサイドで解決することは不可能です。
社内サイトや一部の会員サイトであれば大きな問題にはならないかもしれませんが、SEOの考慮が必要なタイプのサイトではSPAだけではどうしよもないのが現状のようです。

「不可能です(キリ」とか強い言葉を使っておきながら間違っていました。
厳密にいえば存在しないURLにアクセスした場合に404を返すのはブラウザの仕様なのでこれをVue.jsで制御するのはやはり不可能ですが、本来の目的であるパーマネントリンクを200で返すことはできました。

Twitterで教えてもらったのですが単純にVueRouterでHistoryモードではなくHashモードにすれば良いですね。

http://oursite.com/user/idではなくハッシュモードのhttp://oursite.com/#/user/idにすれば、これはindex.htmlへのアクセスなのでVueRouterをおのずと通ります。またVue内のページは難しいですがあからさまに間違ったURLは404を返せるので、これで十分な事は多いですね。

なんとなく「#」が付くのが気持ち悪かったのと、ハッシュモードをHTML5 History API非互換ブラウザのサポートのための機能くらいに考えてたので見落としてました。マニュアル良く見るべし。

ソフト404やOGPタグの問題は解決しませんが、圧倒的にHistoryモードよりも簡単に運用できるようになりSSR/SSGなしで運用できる部分がぐっと増えますね!

そしてSSRへ

SSRとは「Special Super Rare」 ---- では無く「Server Side Rendering」の略称です。
今回上げた「Twitterカードの動的生成」や「嘘つきのHTTPステータス」を解決する一つの道がSSRです。

SSRはまあ文字通りなのですがようは本来クライアントでレンダリングする想定のVue.jsをサーバサイドでレンダリングする方式です。
つまり、従来型のテンプレートエンジンとして利用するってことですね!

正直これはあまり解決策になってないというか、これを本格的に選ぶならそもそもSPAとしてJSで頑張らずにJavaとかRubyで書くよ的な感じがあふれてくるので避けたい道です。まあ現状それ向けのJS以外の実装は確かに少ないけれど。。。

もう一つの道がプリレンダリングです。実行時ではなくビルド時にコンテンツを生成するという事ですがようは「静的サイトジェネレータ (SSG)」ですね。

こちらはCDNにデプロイできますし、アプリケーションサーバも不要なのでVue.jsなどでも可能であればこちらが推奨という事のようです。
ただ、データベースの中身を参照しながら全データを出力するのはそれなりに時間がかかりますし何よりデータもデカい。
そして、更新頻度の激しいページだとそのたびに非同期でサイトジェネレータを回す必要があるのでこれも万能の解決策とは言えない感じですね。。。
小規模ならそれなりにありな気がしますし、中規模/大規模でも構成を工夫すればスケールさせる事は出来る気もします。シャーディング的発想で裏側を細かく分けたり。
あと、認証後にしかアクセスできないページはセキュリティ的に生成できないですね。「それはSEOも不要でしょ!」って割り切りなのかな。

まだ試してないけどNuxt.jsなんかはこの辺に上手い回答を出してるんだろうか? Vue.jsだけで良いかと思ってたんだけど、そっちも見てみる必要がありそうです。

フロントエンジニア「だけ」で良いという幻想

とりあえずVue.jsや恐らくほかのSPAフレームワークも開発しやすいことは間違いないのですが、本番へのデプロイ方法がとても気になります。
SEOをまともに考えるとSSRかプリレンダリングは必須であり純粋なフロントエンジニアだけだと荷が重いのでは? と思ってしまいますね。

昨今、フロントエンジニアとFirebaseがあればバックエンドやインフラ担当はいらないという風潮もあるけどそうとも言い切れなさそう。より正確にいえばこの文脈のフロントエンジニアはFunctionsをマネージドnode.jsに見立てて使ったり、ロードバランサやリバースプロキシを設定するスキルが普通にあるんだろうな。つまり実際のところフルスタックよりのスキルセット。まあ、呼び方は何でも良いから構わないけど技術選定時には誤解しないように注意、と。

そんなわけで、少なくとも中/大規模環境でSEOをまともに考えたサービスを運用するなら、結構サーバサイドに工夫は必要なので「猫も杓子もSPAだけど、ほんとみんな本番で運用してるの? 地味にめんどいぞ?」って思ったわけです。良いものなんですけどねぇ。

今後のSPAに必要な気がすること

まあ、このあたりはフロントのつよつよエンジニアの人々が日々課題に思ってるでしょうからきっと解決するんでしょうけど、あえて門外漢として考えるとするとSPA向けのスマートな静的ホスティングサービスが重要になってくるのかな、と。

「Twitterカードの動的生成」にしろ「嘘つきのHTTPステータス」にしろ原理的にクライアントサイドだけで解決を考えるのは筋が悪いのでサーバサイドに何かしらの仕組みがいります。

かといってSSRは一周回って先祖返りしてるだけだし、プリレンダリングも生成パフォーマンスの課題があります。まあ少しでもそれを解決するためにSvelteとか新しいタイプのFWが台頭してるのかな? とはいえ限度というかちゃんと運用するための構成が複雑過ぎる気がします。

なので、ルーティングの解釈とTwitter CardのようなOGPの動的生成をVue.jsなどのクライアントサイドフレームワークのコードを読んで自動的に解釈してくれるホスティングサービスが今後必要になってくるんじゃないですかね。とりあえず、デプロイしておけば後はOK的な。知らないだけですでにありそうな気もするけど。

Vue Routerは静的な情報だから結構簡単に作れそうだけど、OGPの動的生成は汎用的なのはちょっと難しいかも? まあ、ある程度規約を決めればできる気はする。今度作ってみるか。。。

まとめ

とりあえず、ここ半年くらいVue.jsを触ってみてようやくお道具箱に入ったというか、やりたい事はある程度イメージ付くし過去のコードの流用で済むところも増えました。

最初は自分がJS触ってた頃からすると相当ジャンプがあり少し戸惑いましたが、慣れてくると流行るだけあってやはり使いやすさはありますね。特に画面全体を描画しないので当たり前だけどレスポンスは良いです。HTML風のテンプレートなので参入障壁も小さいですしね。バイバイjQuery!

一方で解決したと思い込んでいたSEOはそうでもなく、むしろみんなどうやって本番運用してるの?? という疑問があふれてきた次第です。気になって今使ってるzenn.devのHTMLを見たらNext.jsをプリレンダリングかSSRで利用されてる感じはするので、その辺が主流なんだろうか?

そもそも、従来型のWebサイトを作ろうとすると無理があるのであって「今はサイトデザインはこうする」というパラダイムシフトをキャッチし損ねてる可能性も多いにあります。

そんなわけでまだこの長いSPA坂を上り始めたばかりですが、打ち切らずに適度にキャッチアップに努めていこうと思う次第です。

それではHappy Hacking!

Discussion

Kaido IwamotoKaido Iwamoto

検索エンジンのことを考える必要がないアプリケーションも多いので、そういうところではSPAは使いやすいし強みを発揮しやすいかなぁと思います。

また、デプロイ先についてはNetlify, Vercel, Firebase Hostingなどがよく使われているようですよ。

kodukikoduki

コメントありがとうございます。
そうですね、社内サイトや一部の会員系サービス(特にB2B SaaS)ならSEOやOGPの動的生成は優先度低いので良さそうですよね。
デプロイ先ははてブでもVercelが勧められてたので試してみます。

rithmetyrithmety

SPA の利点として下記の2つがあると思っています

  1. 静的ファイルで構成される(高速なレスポンスと安価な運用
  2. ページ移動が高速

SSR は SEO のために 1. を(全部あるいは部分的に)捨てる技術だと思っています

2. の利点を Rails などのプロジェクトに導入するライブラリとして
たとえば Turbolinks があるみたいですね
(私はつかったことがないのですが html をやりとりするらしいです

でかいサイトを SSG したくないという考えに対し
たとえば Next.js で Incremental Static Regeneration と呼ばれる技術が開発中のようです
https://nextjs.org/blog/next-9-4#incremental-static-regeneration-beta
Gatsby にも Incremental Builds が存在するらしいです
https://www.gatsbyjs.com/docs/conditional-page-builds/

kodukikoduki

ありがとうございます。

そうですねSPAには良いところが沢山あり、SSRは少なくとも1のメリットを失ってしまうのでなんだかなぁ、と言うのが今のところのイメージですね。シンプルさが失われてしまう的な。無論、必要ならするのですが。。
2に関してはその通りで普通のサイトっぽいつくりにしても全体を更新してるわけではないのでサクサク描画されるのが良いですね。

Incremental Static Regenerationは良さそうですね。
SNSみたいな更新頻度が多いケースに耐えれるかが気になってましたけど、まあmemcachedとかでページキャッシュ作るのと同じようにやれば良いかもですしね。