大変!スクリプトのバージョン違ってますよ

Webアプリを修正したら

Webアプリを修正/変更して公開したら突然、意味不明なエラーが多発し始めることがあります。

こんな時、通常は「再読み込みしてください」とお願いします。修正/変更前の古いJavaScriptファイルをWebブラウザがキャッシュしているのが原因であれば、これで解決です。

以上、終わり...で終わるのであれば、ことは簡単です。しかし、世の中そう甘くはありません。

実際に、顧客のパソコン上で、意味不明なエラーが多発したら、それこそ大変なことになります。

今まで聞いたことのない部署の、聞いたことのない偉いさんから、訳の分からないメールがじゃんじゃん飛んできます。

やれ「なぜXXXXXXXXXXXXだ!」とか、やれ「いつもXXXXXXXXXXXX」とか、挙句の果てに「※※※※※※※※※※※※※※※※」とかとか。

えーい鬱陶しい!!!

こうならないために

注意深い設計者なら、そもそもこのような状況に陥らないでしょう。解決策はいくらでもあるのですから。

JSPやPHPなどの、動的にページを編集できる環境であれば、スクリプトやスタイルシートを、全てHTML内に埋め込んでしまえば解決です。

では、静的なHTMLの場合はどうでしょう。

この場合、ページ専用のスクリプト以外を埋め込むことはできない(やったら大変なことに...)ので、スクリプトファイルの名前に、バージョン番号を入れることになります。

XXXXXX_1.0.0.8.js

といった感じになります。修正や変更が入る度にバージョンを上げます。

しかし、大量にあると

スクリプトのファイル名にバージョン番号を入れるのは手っ取り早いのですが、HTMLが100ページ・300ページ、いや1000ページあったら、どうなるでしょう?

最初に作るときは、別に問題ないかもしれません。テンプレートを作って、それをコピーして使えば良いのですから。しかし、一度スクリプトに変更が入りバージョンが変わった時が大変です。

全てのページを修正しなくてはなりません。同じディレクトリに存在すれば、コマンド一発かもしれませんが、複数のディレクトリどころか、複数のマシン・複数の国にまたがって存在するかもしれません。

管理ツールで一括管理されていれば問題ないかもしれませんが、どれだけのサイトが正しく管理されているのでしょうか?

キャッシュなんかさせない(使わせない)

キャッシュをさせない(正確には使わせない)という選択肢があります。Webブラウザがキャッシュした古いファイルを使いさえしなければ、スクリプトのファイル名にバージョン番号を入れる必要もなく、大量のHTMLを修正する必要もありません。

方法は簡単です。スクリプトでスクリプトをロードするコードを書けば良いのです。えっ「訳がわからん」?

では、以下のコードをご覧ください。

スクリプトをロードするスクリプト

このスクリプトの肝は、2行目の最後のnew Date().getTime()です。このコードはミリ秒単位の現在時刻(正確には1970年1月1日午前0時(GMT)を基点とた経過時間のミリ秒)を返します。

スクリプトファイル名の後ろに、クエリとしてミリ秒単位の現在時刻を付与すれば、毎回異なるURLでリクエストすることになるので、WebブラウザのキャッシュにはそのURLは存在せず、キャッシュが使われてしまうことはありえません。

この方法は、スクリプトだけではなくスタイルシートにも使えます。これで、ある時突然画面が壊れるという無残な結果も回避できます。

スタイルシートをロードするスクリプト

これで、Webブラウザがキャッシュの古いファイルを使ってしまうことによる問題は解決されました。

しかししかし、昨今のWebアプリは...

GUIの出来を競うかのような昨今のWebアプリでは、巨大なJavaScriptファイルをいくつもロードすることが当たり前のようになっています。実際のところ、汎用的に設計され実装されたJavaScriptのライブラリ群は、メガバイトレベルになりつつあります。

改行やコメント・余分な空白を削除し、メソッド名やフィールド名を変更して、サイズを縮小するコンパイル(と言うか圧縮)を施すこ とで、サイズを小さくする方法も有りますが、エラーが発生しようものなら障害箇所の特定が難しく(不可能に近い?)、障害対応にシビアなシステムに適応す るのははばかられます。

できれば、一回ロードしたら次からはキャッシュを使って欲しいものです。そして、修正や変更があった場合は、確実に最新のファイルをロードしなおしてもらいたいものです。そして、その度にロードするファイル名を書き換えたくなど有りません。

やってみましょう

思い通りのファイルをロードさせる

考え方は簡単です。サーバ上で定義した通りのファイル名で、Webブラウザがスクリプトやスタイルシートをロードしてくれるようにするのです。

このコンテンツの「キャッシュなんかさせない(使わせない)」で紹介した方法を使えば、任意のファイル名でスクリプトやスタイルシートをロードさせることができます。

ミリ秒単位の現在時刻を使う代わりに、ロードさせたいバージョン番号をファイル名に付与することが出来れば、全てが解決します。

以下のスクリプトで、本体のスクリプトをロードします。すでに紹介した、キャッシュを使わせないためのスクリプトと、ファイル名を除き同じものです。

スクリプトをロードするスクリプトをロードするスクリプト

autoload.jsの内容は、(例えば)以下のようになります。

スクリプトをロードするスクリプト

スクリプトやスタイルシートの修正が必要となったら、ファイル名のバージョンを上げて、autoload.jsの内容を書き換えれば、関連した全てのページのバージョンアップが完了します。

しかし、実際にシステムをメンテする場合、特定のライブラリを使っているプログラムを一気に全てバージョンアップするなどという事は、通常ありえません。

出来る限り影響範囲を小さくしたい(テストの範囲を減らしたい)というのが、正直なところでしょう。

これを実現するためには、どのプログラムのどのページに、どのバージョンのどのライブラリをロードさせればよいかを、サーバ側で管理することになります。

また、クライアント側では、自分がどのプログラムのどのページであるかを認識させ、それをサーバに通知する必要があります。

ここからは、当社で現在使用している内容をベースに説明します。

但し、ライブラリ管理のような機能は、ユーザ毎・システム毎に考え方や方法が異なると思われるので、当社のライブラリの実装(このコンテツの内容も含めて)は、ほんのサンプルレベルであることを、ご了承ください。

まずは、クライアントに認識させる

クライアントに自分自身を認識させるために、meta要素を使用します。どのように分類するかは、それぞれのコンテンツで考え方もいろいろでしょうから、少々オーバースペック気味にして、汎用的なライブラリでもある程度網羅できるようにしてみました。

META要素による分類

name属性は、バッティングを避けるために、当社のドメインを使用していますが、要は「サイト」レベル、「コンテンツ」レベル、「パッケージ」レベル、「ページ」レベルに分けて、それに加えてバージョンも指定できるようにしています。

サイトやコンテンツが実際の何に該当するかはそれぞれですが、例えばサイト1が日本向け、サイト2が米国向け。または、サイト1がA社向け、サイト2がB社向けと言ったところでしょうか。

コンテンツは、Webアプリケーションや静的なHTMLの塊を想定しています。それぞれのコンテンツは、複数のパッケージで構成され、パッケージは複数のページを含みます。

全てのページにこの指定をすれば、クライアントは自分自身が何者かを認識できることになります。

当社で使用しているライブラリによる実例のご紹介

スクリプトをロードするスクリプトをロードするスクリプトの実例

jsautoloader.jsというファイルが、先に述べた「スクリプトをロードするスクリプトをロードするスクリプト」に該当します。このスクリプトは、meta要素を検索し該当するname属性の要素からcontent属性の内容を取得して管理します。

そして、同じフォルダにあるjsvctrl.jsを、クエリにmeta要素に指定されていた内容とミリ秒の日時を付けてロードします。

jsvctrl.jsに、meta要素に指定されていた内容をクエリとして付与するのは、jsvctrl.jsの実体がサーブレットやPHPで、DBから必要なライブラリファイル名を取得して、スクリプトを動的に編集するようなケースを想定しているためです。

サンプルコードは、実際に使用しているものから、幾分簡略化されていることをご容赦ください。

jsautoloader.js

やっていることは簡単です。

10行目でMETA要素を全て取得して、11行目以降でそれぞれの名前に該当する情報を取得して、変数に格納します。

34行目以降で、自分自身(jsautoloader.js)をロードしたパスから、親ディレクトリまでのフルパスを取得して、baseUrlとしています。

48行目以降で、jsvctrl.jsのフルパスを編集して、ロードするためにscript要素を作成します。

58行目以降は、格納している情報を返却するための、メソッドです。

スクリプトをロードするスクリプトの実例

jsvctrl.js

こちらも、難しいことはしていません。

5行目6行目で条件を判断し、ロードするスクリプトやスタイルシートの情報を配列として取得します。

ファイルのパスは、jsautoloader.jsで取得した、baseUrlからの相対パスです。

後は、それをロードするために、scriptlink要素を作成します。

HTMLの実例

使い方は、下記のようになります。

jsautoloader.jsの使い方

異なるバージョンのライブラリを読み込む簡単なサンプルです。

バージョン1.0のライブラリをロードするサンプル

上記の、「jsautoloader.jsの使い方」で照会したHTMLです。

バージョン1.1のライブラリをロードするサンプル

「jsautoloader.jsの使い方」で照会したHTMLの、7行目のバージョンを、0.1.0.1にしたHTMLです。

もう少し先へ

簡単にロードするスクリプトやスタイルシートを管理できるようになったので、もう少し先を考えてみます。

最近(当社が気にし始めたのが最近)、なんと言っても面倒なのが、モバイル対応です(遅っ)。

当社のサイトをモバイルで見ている人は、さほど多くないと思われるのですが、自分たまに見ても、ウザっと思うので少しずつ対応を勧めています。

当社のサイトは、特に凝ったことをしているわけではないので、コンテンツによってはモバイル対応もそれ程難しくはありません。

それでも、スクリプトやスタイルシートの切り分けは必要となるので、このコンテンツの考え方は便利です。

モバイル判定

当コンテンツの趣旨とは離れますが、PCとモバイルの判定は、どうやるのが正しいのでしょうねえ?

ネットで検索してみると、userAgent文字列から判断する方法が一般的のようですが、簡単に変更できてしまうので、少し不安です。

尤も、ユーザが意図してモバイルなのにPC用のコンテンツを見たいといった場合は、それに沿うのが正しいのでそのように動作すべきです。

現状は、window.onorientationchangeの有無で判定しているので、全くユーザの意図には沿えていないのですが、userAgent文字列の判定はいろいろ問題が多いし、かと言ってわけのわからないライブラリなど使いたくないしで、今後も試行錯誤が続きそうです。

そもそも、window.onorientationchangeの有無で判定なんて、Mac版のSafariは大丈夫?とか、ノートPC上のWebブラウザは大丈夫?とか、未確認の内容多すぎですし。

Firefox ユーザーエージェント文字列リファレンスにあるように、userAgent文字列から"mobi"を探すというのが、正解なんでしょうか...

気を取り直して

当社で実際に使用しているjsautoloader.jsには、meta要素を取得する前に下記のコードが存在します。

モバイル判定

com_yscjp_vctrl_PACKAGEをPCかモバイルかの判定に使用します。

jsvctrl.jsでは、モバイル向けの場合、モバイル専用のスクリプトとスタイルシートをロードさせるようにします。

モバイル専用ファイルのロード指示

yscjp_mobile.jsの中で、viewportの指定などを行います。