Hatena::ブログ(Diary)

on the center line.

2008-10-03

XMLHttpRequestオブジェクトでリダイレクトをハンドリングする方法

| 10:05

以前に、同じタイトルのエントリを書いたのですが、それよりもスマートなやり方がわかったので、書いておきます。

やりたいことについては前のエントリで書いたとおり。

  1. XMLHttpRequestで送ったリクエストが、サーバで認証エラー(セッションタイムアウトなど)となる
  2. このときサーバは認証エラー(=401)を返すのではなく、ログインページへのリダイレクトを返す
  3. クライアントXMLHttpRequest)は、リダイレクト応答をうけとり、自動的にリダイレクト先を追跡する
  4. 結果、クライアントログインページのHTMLを取得する(このときステータスコード=200)
  5. しかし、そもそもクライアントJSON形式のデータを期待しているためスクリプトエラーとなる

前のエントリでは、XMLHttpRequestでレスポンスヘッダが使えることに着目して、"Content-Type"ヘッダが"text/html"だったらページを強制的に遷移させる方法でした。しかし、この方法は「"Content-Type"ヘッダが"text/html"である正常なレスポンス」があったら使えません。そこで以下の方法を思いつきました。4. および 6. のところが今回紹介する方法になります。

  1. XMLHttpRequestで送ったリクエストが、サーバで認証エラー(セッションタイムアウトなど)となる
  2. このときサーバは認証エラー(=401)を返すのではなく、ログインページへのリダイレクトを返す
  3. クライアントXMLHttpRequest)は、リダイレクト応答をうけとり、自動的にリダイレクト先を追跡する
  4. ログインページでは、レスポンスヘッダに "AjaxRedirect: /Login.jsp" を追加する
  5. 結果、クライアントログインページのHTMLを取得する(このときステータスコード=200)
  6. クラインアンは、レスポンスヘッダをチェックし、"AjaxRedirect" ヘッダが設定されているので、その値のURLを追跡する

以上で、XMLHttpRequestを使ったリダイレクトの完成。サンプルコードも載せておきます。

<%
response.setHeader("AjaxRedirect", request.getContextPath() + "/Login.jsp");
%>
httpRequest.onreadystatechange = function() {
  if (httpRequest.readyState == 4 && httpRequest.status == 200) {
    var redctUrl = httpRequest.getResponseHeader("AjaxRedirect");
    if (redctUrl != null) 
      // リダイレクトURLが設定されていれば追跡
      window.location.href = redctUrl;

世捨て人世捨て人 2017/09/05 22:22 「しかし、この方法は「"Content-Type"ヘッダが"text/html"である正常なレスポンス」があったら使えません。」??何故!?
サーバーがtext/htmlを返してきたのだから、そのままページを表示すればいいのでは!?

2008-07-23

JavaScriptでブラウザの判別(各種JavaScriptライブラリの判定方法を抜粋)

| 09:52

JavaScriptライブラリを使わずに開発しているときに、ブラウザの判定をするのって意外と悩むので、調べた結果をまとめておきます。Prototype.jsJQuery、MooTools、Dojo について調べました。

Prototype.js の場合

  Browser: {
    IE:     !!(window.attachEvent && !window.opera),
    Opera:  !!window.opera,
    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
  },

JQuery の場合

var userAgent = navigator.userAgent.toLowerCase();

jQuery.browser = {
	version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [])[1],
	safari: /webkit/.test( userAgent ),
	opera: /opera/.test( userAgent ),
	msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ),
	mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent )
};

●MooTools の場合

var Browser = new Hash({
  Engine: {name: 'unknown', version: ''},
  Platform: {name: (navigator.platform.match(/mac|win|linux/i) || 
                    ['other'])[0].toLowerCase()},
  Features: {xpath: !!(document.evaluate), air: !!(window.runtime)},
  Plugins: {}
});

if (window.opera)
  Browser.Engine = {name: 'presto', version: (document.getElementsByClassName) ? 950 : 925};
else if (window.ActiveXObject)
  Browser.Engine = {name: 'trident', version: (window.XMLHttpRequest) ? 5 : 4};
else if (!navigator.taintEnabled)
  Browser.Engine = {name: 'webkit', version: (Browser.Features.xpath) ? 420 : 419};
else if (document.getBoxObjectFor != null)
  Browser.Engine = {name: 'gecko', version: (document.getElementsByClassName) ? 19 : 18};
Browser.Engine[Browser.Engine.name] = Browser.Engine[Browser.Engine.name + Browser.Engine.version] = true;

Dojo の場合

var dua = navigator.userAgent;
var dav = navigator.appVersion;
var tv = parseFloat(dav);

d.isOpera = (dua.indexOf("Opera") >= 0) ? tv : 0;

var idx = Math.max(dav.indexOf("WebKit"), dav.indexOf("Safari"), 0);
if(idx) {
  d.isSafari = 
    parseFloat(dav.split("Version/")[1]) || 
    ( ( parseFloat(dav.substr(idx+7)) >= 419.3 ) ? 3 : 2 ) || 2;
}
d.isAIR = (dua.indexOf("AdobeAIR") >= 0) ? 1 : 0;
d.isKhtml = (dav.indexOf("Konqueror") >= 0 || d.isSafari) ? tv : 0;
d.isMozilla = d.isMoz = (dua.indexOf("Gecko") >= 0 && !d.isKhtml) ? tv : 0;
d.isFF = d.isIE = 0;
if (d.isMoz) {
  d.isFF = parseFloat(dua.split("Firefox/")[1]) || 0;
}
if (document.all && !d.isOpera) {
  d.isIE = parseFloat(dav.split("MSIE ")[1]) || 0;
}

Prototype.jsJQueryはわりとシンプルな判定方法を使っているのに対し、Dojoはだいぶ複雑ですね。その代わりDojoでは、isMoz、isFFが区別できたり、isKhtml、isAIRなんてのがあったり、そもそもブラウザの判別がtrue/falseの2択ではなくてバージョン番号までわかったりと、かなり高機能になってます。

ま、用途に応じて使い分けましょう、と。

2008-06-16

IEでFunction.prototype.extendsというプロパティが使えない。

| 16:52

JavaScriptで継承を実現するために、

Function.prototype.extends = function(superObj) {
  for (var prop in superObj)
    this.prototype[prop] = superObj[prop];
};

このようなメソッドを作ってました。FireFoxで開発している分には全く問題なく動作していたのですが、IEでテストしてみたところエラーとなってしまいました。

で、調べてみたところ、下記のエントリで全く同じ問題を紹介してました。

IEでextendsって名前が使えない

MSのサイトに予約語の一覧がのってますが、「将来的に使用される」のところは要注意ですね。exportとかついつい使ってしまいそう・・・。

http://msdn.microsoft.com/ja-jp/library/cc391861.aspx

そもそも、ブラウザによって予約語まで違うとは思わなんだ。orz

2008-05-13

XMLHttpRequestオブジェクトでRedirectをハンドリングするには?(リダイレクトを拾う方法)

| 21:45

XMLHttpRequestオブジェクトから送信したリクエストに対し、サーバリダイレクト応答(ステータスコード=302)を返したときのハンドリングってどうやればいいんだろうか?と悩んだのでその記録。

解決したかったのは、以下のような問題。

  1. XMLHttpRequestで送ったリクエストが、サーバで認証エラー(セッションタイムアウトなど)となる
  2. このときサーバは認証エラー(=401)を返すのではなく、ログインページへのリダイレクトを返す
  3. クライアントXMLHttpRequest)は、リダイレクト応答をうけとり、自動的にリダイレクト先を追跡する
  4. 結果、クライアントログインページのHTMLを取得する(このときステータスコード=200)
  5. しかし、そもそもクライアントJSON形式のデータを期待しているためスクリプトエラーとなる

調べてみたところ、XMLHttpRequestリダイレクト応答をうけとると何事もなかったかのようにリダイレクト先にリクエストを送信してしまうようだ。そのため、クライアント側ではリダイレクトされたかどうかが判別できない(ステータスコードは常に200)。これでは困るのでステータスコード以外について調べてみたところ、レスポンスヘッダーの取得はできるようだ。これは都合がいい。

というのは、今回問題が発生したWebアプリではXMLHttpRequestのレスポンスは『必ず』JSON形式(=text/plain)としている。なので、text/htmlのレスポンスが返されたら、それはリダイレクトとみなすことができる。

完全な解決策とはいえないけど、以下のコードで問題解決。

httpRequest.onreadystatechange = function() {
  if (httpRequest.readyState == 4 && httpRequest.status == 200) {
    var cType = httpRequest.getResponseHeader("Content-Type");
    if (cType.toLowerCase().indexOf("text/plain") == -1) 
      window.location.href = url;                                             ← ここで認証エラーとなったリクエストを再送→リダイレクト
    // 以下、省略

2008-05-01

静的HTMLで、インクルードを実現するためのJavaScript

| 19:01

ローカルディスク上に存在する静的HTMLで、外部ファイルをインクルードするためのスクリプト(つまり、XMLHttpRequestオブジェクトの代わり)。

以前にも同じようなエントリを書いたのですが(http://d.hatena.ne.jp/kenpoco/20080228/1204198090)、いくつか不具合があったので修正してます。

  • IE6,7に対応(前回はFirefox2のみ)。
  • インクルードするファイルの拡張子が.HTMLと.TXTで動作が異なっていたので、その対応。
  • インクルードした後で、その内容を変更するための仕組みを追加。

最近HTMLを作る機会が多いので自分で使ってみてるのですが、けっこう重宝してます。全ページに同じようなヘッダー部分がある場合なんかに、ヘッダー部分を1ファイルにまとめておけるので、その後の修正がかなり楽。

【使用例】
-----------------------
 インクルード部分
-----------------------
<div>
   <script type="text/javascript" >
      include("a.html");    ←HTML内のインクルードしたいところに記入
   </script>
</div>

-----------------------
 インクルードされるファイル(a.html)
-----------------------
<div style="border: solid 1px #000000;">hello;</div>

-----------------------
 インクルード結果(表示後にinnerHTMLで確認)
-----------------------
<div>
   <script type="text/javascript" >
   </script>
   <div style="border: solid 1px #000000;">hello;</div>  ←includeした内容が<script>タグの下のところに入る
</div>
【関数本体】
-----------------------
function include(filename, afterfunc) {

  include.seq = (include.seq)? include.seq + 1: 1;

  var id = new Date().getTime() + "-" + include.seq;
  var inc = document.createElement("iframe");

  inc.id = "inc-" + id;
  inc.src = filename;
  inc.style.display = "none";
  document.write("<span id=\"" + id + "\"></span>");
    
  var incfunc = function() {
    
    var s = (function() {
      var suffix = (n = filename.lastIndexOf(".")) >= 0 ? filename.substring(n): "default";
      if (suffix == ".html") return inc.contentWindow.document.body.innerHTML;
      if (suffix == ".txt") return inc.contentWindow.document.body.firstChild.innerHTML;
      if (suffix == "default") return inc.contentWindow.document.body.innerHTML;
    })();

    var span = document.getElementById(id);

    var insertBeforeHTML = function(htmlStr, refNode) {
      if (document.createRange) {
        var range = document.createRange();
        range.setStartBefore(refNode);
        refNode.parentNode.insertBefore(range.createContextualFragment(htmlStr), refNode);
      } else {
        refNode.insertAdjacentHTML('BeforeBegin', htmlStr);
      }
    };

    insertBeforeHTML(s.split("&gt;").join(">").split("&lt;").join("<"), span);
    document.body.removeChild(inc);
    span.parentNode.removeChild(span);
    if (afterfunc) afterfunc();
  };

  if (window.attachEvent) {
    window.attachEvent('onload', 
      function() {
        document.body.appendChild(inc); 
        inc.onreadystatechange = function() { if (this.readyState == "complete") incfunc(); };
      });
  }
  else {
    document.body.appendChild(inc);
    inc.onload = incfunc;
  }
}

useruser 2009/07/29 10:16 ありがとうございます!
とっても便利です。使用させていただきます

yahooyahoo 2009/08/26 17:49 とても便利なのですが、こちらieで確認すると表示されますが、firefox3で確認するとインクルード部分表示されません。
そういうものでしょうか?

ゆきこゆきこ 2009/10/05 15:12 大変便利に使わせていただいております。
しかしながら、オペラでは見られないみたいで・・・。何か対応があれば教えていただけると助かります。あつかましいおねがいで失礼致します。

zeroxonezeroxone 2009/10/29 10:20 IE7で拡張子がtxtだとソースの一部分(最初の方だけ。削除すると下も表示される)しか表示されません。しかし拡張子をhtmlに変えたら問題なく表示されました。これって何が違うのでしょう?
firefoxはOKでした。

ぼんぼん 2009/11/29 22:45 使わせてもらってます。ありがとうございます。js でできるんですね。
ただinclude2か所試しましたが、1か所は反映されませんでした。
2か所は無理なんでしょうか?

ぶるるぶるる 2009/12/16 04:30 使わせていただてなんですが、firefox3.5だとまるっきり映りません。35行目にエラーと出ます。document.body.removeChild(inc); このあたりだと思うのですが。

老婆心老婆心 2010/09/04 21:53 include される HTML 内に、form の action が、あると、button の submit も、機能しないのは、何故でしょうか?

MasaYan21MasaYan21 2014/01/26 01:34 Chromeだと使えません。ローカルファイルだとアクセスに制限があるとか、ないとか?(T_T)
Uncaught SecurityError: Blocked a frame with origin "null" from accessing a frame with origin "null". Protocols, domains, and ports must match.

さぶろーさぶろー 2014/09/10 10:19 すばらしい!動かないっていう人はコードを読ませてもらいましょう。勉強になりますよ。なにはともあれ客先に提出用のサンプルを作るには最高です。ありがとうございます。