しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure AD B2C にカスタムドメインを設定して MSAL (C# / JavaScript) から使ってみた

恐らく Azure AD B2C を使っている人全員が待ち望んでいたカスタムドメイン対応ですが、Front Door と組み合わせる形にはなりますが Preview として公開されました。

単純に Front Door をリバースプロキシとして使うだけなのですが、割と現実的な落としどころな実装になった感があります。Front Door 分の課金は必要になりますが、こればかりは仕方ありません。

カスタムドメインの設定方法のドキュメントが公開されているので、これに従って実際に設定する方法と、設定後の Azure AD B2C を MSAL から使うところまで試してみました。

今回のカスタムドメイン対応と同じタイミングで、Azure AD B2C のログイン画面を iframe で使うための機能も Preview として公開されています。

SPA ではログイン時に必ず Azure AD B2C 側へのリダイレクトが必要でしたが、iframe を使うことで画面遷移なしにログインさせることが出来ます。UI のカスタマイズ性も向上するでしょう。

流石にこっちは試しませんが、暇なタイミングで UI に詳しい人と弄ってみたいと思います。

Active Directory にカスタムドメインを追加

とりあえずドキュメントに従って Active Directory に設定したいカスタムドメインを追加します。この辺りは Azure AD を使っていれば設定したことがある人は多そうです。

通常なら Azure AD B2C などにはサブドメインを割り当てると思いますが、Active Directory への追加時には Apex ドメインを最初に登録する必要があります。

Apex ドメインを追加すると検証プロセスが走るので、検証後にサブドメインを追加可能になります。

f:id:shiba-yan:20210317133802p:plain

今回は使い道のないドメイン login.daruyanagi.com を使うので、daruyanagi.com から先に追加します。

f:id:shiba-yan:20210317133819p:plain

使用するドメインが検証済みになれば Azure AD B2C 側の設定は完了です。この設定は Front Door で転送されてくるホスト名の許可リストになるようです。

Make primary を押したくなりましたが、いろいろと壊れそうな気がしたので止めておきました。

Front Door にカスタムドメインと B2C へのルーティングを追加

Azure AD B2C に使用するカスタムドメインを追加すれば、後は Front Door にカスタムドメインを割り当てて、B2C へのルーティングを追加するだけです。

特に説明も必要ない気はしますが、一応スクリーンショットを撮っておいたので簡単に書きます。Front Door で新規 Backend pool を作成して、Backend として b2clogin.com で終わるホスト名を設定します。

f:id:shiba-yan:20210317133857p:plain

それ以外はデフォルトのままで問題ありません。Status が Enabled になっていることだけは確認します。

Backend pool では Health probes を無効化して保存します。単純にチェックの必要はありません。

f:id:shiba-yan:20210317133904p:plain

後は Front Door の Frontend にカスタムドメインと証明書を設定して、ルーティングルールで先ほど作成した Backend pool を設定するだけで、Azure AD B2C のカスタムドメイン化が完了します。

実際には Azure AD に設定したカスタムドメインでもアクセス可能にしているだけなので、Azure Portal には b2clogin.com のエンドポイントしか表示されませんが、手動でホスト名を書き換えて確認します。

f:id:shiba-yan:20210317133914p:plain

ログインを行って取得できた id_token を確認すると iss もカスタムドメインで設定したものになっています。ここはログインに使用したドメインがそのまま使われるようになっているようです。

f:id:shiba-yan:20210317133921p:plain

これまで b2clogin.com を使っていたものをカスタムドメインに置き換える場合には、iss のチェックで既存のトークンがエラーになる場合がありそうなので、リリース計画を立てる必要がありそうです。

Identity Provider 側の設定を修正する

Facebook や Google といった OAuth 2 / OpenID Connect に対応した Identity Provider を使っている場合は、カスタムドメイン設定後はリダイレクト URL が変わるので、以下のようなエラーとなるはずです。

f:id:shiba-yan:20210317133935p:plain

IdP の設定からリダイレクト URL をカスタムドメインを使用したものを追加するか、変更すればこれまで通りログイン可能です。忘れやすい部分だと思うので注意したいです。

f:id:shiba-yan:20210317182947p:plain

リダイレクト URL は複数設定できることが多いので、変更ではなく追加のが安心です。

MSAL からカスタムドメインで利用する

最後に実際のアプリケーションから使うことを考えて、MSAL を使って試しておきました。まずは良く使いそうな MSAL.js でカスタムドメインを試してみます。

Azure AD B2C と MSAL.js 2.x の組み合わせは、少し前から認可コードフロー + PKCE 対応になっているので非常にすんなり使えます。

カスタムドメインになったからといっても、単純に MSAL に渡す設定に含まれている各 URL をカスタムドメインに置き換えるだけで問題なく扱えました。

const msalConfig = {
  auth: {
    clientId: "<clientid>",
    authority: "https://login.daruyanagi.com/<tenantid>/<policyname>",
    knownAuthorities: ["login.daruyanagi.com"]
  }
}

実行してみるとポップアップがカスタムドメインになっていることが確認できます。もちろんログインも問題なく行えたのでトークンが取得できました。

f:id:shiba-yan:20210317155350p:plain

同様に MSAL.NET でも試してみます。デスクトップアプリケーションで試しているので設定は若干異なっていますが、内容としては MSAL.js の時と同じようにカスタムドメインに置き換えただけです。

WebView だとリダイレクト URL は表示されないので気にする必要はないのですが、折角なのでカスタムドメイン版に置き換えておきました。

var app = PublicClientApplicationBuilder.Create("<clientid>")
                                        .WithB2CAuthority("https://login.daruyanagi.com/tfp/<tenantid>/<policyname>")
                                        .WithRedirectUri("https://login.daruyanagi.com/oauth2/nativeclient")
                                        .Build();

var result = await app.AcquireTokenInteractive(null)
                      .ExecuteAsync();

Debug.WriteLine(result.AccessToken);
Debug.WriteLine(result.IdToken);

Azure AD B2C のアプリケーションのデフォルト設定では、カスタムドメイン版のリダイレクト URL は追加されないので、手動で追加して対応しました。

f:id:shiba-yan:20210317155342p:plain

この設定で MSAL.NET でもカスタムドメインで問題なく動作しました。

全体的に見ると Front Door の設定が必要になるのは若干面倒な感じですが、カスタマイズした WAF を追加できる*1のは魅力になるかなと思いました。GA までにはデフォルトドメインでのアクセス拒否機能が組み込まれて欲しいですね。

*1:Preview 中は WAF を追加するのは推奨されていないっぽいみたいですが

Vue.js 3.0 へのアップグレードを行った作業メモ

これまでも小規模なアプリで Vue.js 2.x を使っていましたが、そろそろ 3.0 へアップグレードするかという気持ちになったので、何故か日付が変わってから作業しました。

該当のコミット、Pull Request は以下のようになりました。一部分だけサラッと使えるので割と好きです。

特に Vue.js 周りの情報は追っていなかった結果として多少はまったので、自分が今後はまらないためにメモを残しておきます。マイグレーション手順とか言う大げさなものではない単なるメモです。

必要なライブラリが分かりにくい

Vue CLI や TypeScript とかを使うまでもない規模なため、これまでも cdnjs で公開されているライブラリを参照して使っていましたが、Vue.js 3.0 になって種類が増えていたので混乱しました。

正直なところファイル名からは推測できなかったので、適当にそれっぽいものを選んだら案の定動きませんでした。ちゃんとドキュメントを確認して必要なライブラリを探すことにしました。

結論としては素の HTML でサラッと使う場合は vue.global.prod.js を選べばよいらしいです。最初に選んだ runtime と付いているものは、テンプレートがコンパイル済みの場合に使うものだったようです。

最近のブラウザだと ES Modules が使えるらしいので、esm-browser が付いたものを選んでモジュールベースで書くことも出来るようですが、そこまでは試しませんでした。

Vue コンストラクタが消滅

とりあえずライブラリだけ 3.0 に入れ替えて試したらコンストラクタが無いというエラーになりました。Vue 2.x では new Vue({...}) をテンプレのように書いてきましたが、この辺りが大きく変わったようです。

アプリの定義周りは特に変わっていないみたいですが、これまでのコンストラクタに相当していた処理は Vue.createAppmount の二つに分離されたようです。

// これまでの書き方
//const app = new Vue({
//  el: '#app'
//  data: {
//    title: 'initial title',
//    body: 'initial body'
//  }
//});

// これからの書き方
const app = {
  data() {
    return {
      title: 'initial title',
      body: 'initial body'
    };
  }
};

Vue.createApp(app).mount("#app");

これに関しては 2.x がオブジェクト作っただけで動いていたのがそもそも気持ち悪かったので、個人的には良い改善だと思いました。副作用バリバリのコンストラクタは好きではありません。

this.$set が消滅

上の修正でアプリケーションは動き始めましたが、次は API から取ってきたデータを View に反映させるために、プロパティに値をセットする部分でエラーになりました。

エラーメッセージを確認すると $set というメソッドは無いと言われていました。

これまでは $set 経由じゃないと変更通知が動かなかったですが、このメソッドが消滅したということはプロパティに直接渡しても良い気がしたので試したら動きました。

正直なところ Knockout.js の Observable の getter / setter よりも酷いデザインだと思っていたので、通常のプロパティと同じように使えるようになったのは便利でした。

// これまでの書き方
//this.$set(this, 'title', 'hello');
//this.$set(this, 'body', 'world');

// これからの書き方
this.title = 'hello';
this.body = 'world';

仕組みは良く分かっていないですが、ドキュメントをチラ見した感じでは内部的に Proxy クラスを自動的に作っていろいろと頑張っているようです。

C#er 的に言うと Entity Framework Core の Lazy Loading Proxy と同じ仕組みっぽさがあります。

Vue.filter が消滅

事前に Vue.js プロフェッショナルの @nahokomatsui から Vue.js 3.0 では Filters が使えなくなると聞いていたので、実行時エラーを出す前にメソッドを呼び出す方向で対応を行いました。

公式ドキュメントにマイグレーション方法が書いてありましたが、異常なほどコードが美しくないと思ったので、この方向での対応は止めておきました。

Vue.js 3.0 の紹介をしている記事をいくつか読んでみましたが、Filters はメソッドを使って対応できるとだけ書いてあって、そのメソッドが何を指しているのか若干不明瞭でした。

結局のところは methods に追加した関数が呼び出せるという話だったようです。

個人的にはパイプでフィルタリングを行う Vue.js 2.x までのコードが結構好きでした。

メソッド呼び出しだと複数のフィルタを実行する場合に括弧が増えて分かりにくいですが、そういった場合には computed を使えということなのでしょう。

<!-- これまでの書き方 -->
<!--
<div id="app">
  <h1>{{ title | toUpperCase }}</h1>
  <p>{{ body }}</p>
</div>

<script>
Vue.filter('toUpperCase', function (value) {
  return value.toUpperCase();
});

const app = new Vue({
  el: '#app',
  data: {
    title: 'initial title',
    body: 'initial body'
  }
});
</script>
-->

<!-- これからの書き方 -->
<div id="app">
  <h1>{{ toUpperCase(title) }}</h1>
  <p>{{ body }}</p>
</div>

<script>
const app = {
  data() {
    return {
      title: 'initial title',
      body: 'initial body'
    };
  },
  methods: {
    toUpperCase(value) {
      return value.toUpperCase();
    }
  }
};

Vue.createApp(app).mount("#app");
</script>

ここまでの作業で自分の利用範囲では 3.0 への移行が完了しました。2.x ではイマイチだと思っていた部分がキッチリ改善されているのは良いですね。今後もカジュアルに使えるライブラリであってほしいです。

パフォーマンスが良くなっているという噂も聞きましたが、自分の利用範囲では関係なかったです。

Azure AD B2C が MSAL.js v2 に対応したようなので試した

MSAL.js v2 の RTM 時には Azure AD B2C は CORS 周りの機能がデプロイされていないため非対応になっていましたが、既存のテナントへの CORS 対応のデプロイが進められているようです。自分のテナントで使えるようになっていたので試しました。

MSAL.js には種類がありますが、今回試しているのは MSAL Browser と呼ばれるブラウザで動くやつです。

これまで MSAL.js と SPA の組み合わせでは Implicit Flow のみ利用可能でしたが、最近は Implicit Flow が非推奨になっているので Authorization Code Flow + PKCE を使うようになっています。

Azure AD B2C の設定画面でも SPA を追加すると、その旨が記載されています。

f:id:shiba-yan:20200905222023p:plain

Authorization Code Flow の場合は CORS に対応する必要があるので、Azure B2C 側の対応待ちという話でした。この辺りに関しては近日中に公式リリースが出る気がします。

とりあえずミニマムなコードで MSAL.js v2 と Azure AD B2C の組み合わせを試します。

流れとしては PublicClientApplication を作成して loginPopup を実行するとポップアップ内でログインが行われます。await でシームレスに扱えるので便利です。

<!DOCTYPE html>
<html>
<head>
  <script src="https://alcdn.msauth.net/browser/2.1.0/js/msal-browser.min.js" integrity="sha384-EmYPwkfj+VVmL1brMS1h6jUztl4QMS8Qq8xlZNgIT/luzg7MAzDVrRa2JxbNmk/e" crossorigin="anonymous"></script>
</head>
<body>
  <script>
    const msalConfig = {
      auth: {
        clientId: "CLIENT_ID",
        authority: "https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/B2C_1_SignUp_SignIn_v2",
        knownAuthorities: ["yourtenant.b2clogin.com"]
      }
    }

    const loginRequest = {
      scopes: ["openid", "offline_access"]
    }

    const msalInstance = new msal.PublicClientApplication(msalConfig);

    async function loginAsync() {
      try {
        const loginResponse = await msalInstance.loginPopup(loginRequest);

        console.log(loginResponse);
      } catch (err) {
        console.error(err);
      }
    }

    loginAsync();
  </script>
</body>
</html>

設定としてはいつも通りの clientIdauthority として User Flow 名付きの URL、そして knownAuthorities が必要です。具体的な値はサンプルコードを見てもらった方が早いです。

このサンプルではトークンのキャッシュを行っていないので毎回ログインされてしまいますが、実際のアプリケーションではトークンの有効期限が切れるまではキャッシュを使うのが基本的な流れです。今回は MSAL.js なら簡単に実装出来るのと、B2C に関係ない部分なので省略します。

適当にホストしてブラウザからアクセスすると、B2C のログイン画面がポップアップ表示されます。

f:id:shiba-yan:20200906003724p:plain

ユーザー登録、もしくはログインを行うと Authorization Code を使ってトークンを取りに行きますが、この時の Pre-flight request で CORS 対応されているのが確認できます。

以前は Pre-flight でエラーになっていたので、トークン取得まで進めませんでした。

f:id:shiba-yan:20200906014628p:plain

もちろん CORS に対応しているので、その後のトークン取得も問題なく行われます。ちゃんと id_tokenrefresh_token などが返ってきていることが確認できます。

f:id:shiba-yan:20200905233002p:plain

実際の loginPopup の戻り値ではクレームがデコードされた状態で入ってくるので、アプリケーションから簡単に使えるのはこれまで通りです。

SPA に MSAL.js v2 を組み込むことで、Azure AD B2C の利用がセキュアかつシンプルになるので良い感じです。ちゃんと PKCE に対応しているのも良いです。

ASP.NET 5 と Visual Studio 2015 では Bower と Grunt が標準になるみたいなので勉強してみた

ASP.NET 5 と Visual Studio 2015 Preview では Bower と Grunt、そして npm へ標準で対応するようになりました。今後は ASP.NET 5 開発で必要となってくる知識になるので、簡単に調べてみました。

既に ASP.NET 公式サイトでは Visual Studio 2015 で Bower と Grunt を使う記事が公開されてます。

ASP.NET Core Blazor | Microsoft Learn

これまでは NuGet だけ使っていれば解決してましたが、今後は .NET のコンポーネントだけが NuGet で提供され、クライアントサイドのコンポーネントは Bower を使うようになるのでしょう。

テンプレートにも Bower / Grunt / npm の設定ファイルが用意されていますが、まずは Bower と Grunt の基本的な使い方を学ぶために ASP.NET 5 のスターターテンプレートを使います。

Bower について

Bower — a package manager for the web

Bower は Twitter が開発したクライアントサイド向けのパッケージマネージャーらしいです。ASP.NET 開発者向けには NuGet と同じようなものという説明がわかりやすいかと思います。

NuGet に JavaScript のパッケージを登録していく手間を考えれば、既にシェアを持っている Bower に乗っかるというのは良い手だと思いました。*1

既に利用者が多く、情報も多いというのはメリットとしてありそうです。しかし、いきなり過ぎて大半の MVC 開発者は( ゚д゚)ポカーンとしてそうな気もしますが。

Bower を Visual Studio から使う

一般的には bower install というコマンドを使えば良いみたいですが、Visual Studio には直接 bower コマンドを叩く機能は用意されていません。なのでパッケージは bower.json で追加する必要があります。

{
    "name": "WebApplication1",
    "private": true,
    "dependencies": {
        "bootstrap": "~3.0.0",
        "jquery": "~1.10.2",
        "jquery-validation": "~1.11.1",
        "jquery-validation-unobtrusive": "~3.2.2"
    },
    "exportsOverride": {
        "bootstrap": {
            "js": "dist/js/*.*",
            "css": "dist/css/*.*",
            "fonts": "dist/fonts/*.*"
        },
        "jquery": {
            "js": "jquery.{js,min.js,min.map}"
        },
        "jquery-validation": {
            "": "jquery.validate.js"
        },
        "jquery-validation-unobtrusive": {
            "": "jquery.validate.unobtrusive.{js,min.js}"
        }
    }
}

Visual Studio 2015 Preview の ASP.NET 5 プロジェクトでは、予め jQuery や Bootstrap などをインストールするような設定が追加されています。今回は underscore.js を追加してみます。

IntelliSense でパッケージ名とバージョンが表示されるので簡単に入力できますが、このあたりは NuGet のように GUI も欲しい気がします。exportsOverride の追加も忘れないようにしないといけないので、やっぱり GUI が欲しいですね。

保存すればソリューションエクスプローラーの Bower に underscore が not installed と表示されます。後は右クリックメニューから Restore Package を選択するとインストールされます。

wwwroot/lib 以下をソリューションエクスプローラーから確認すると、underscore.js がインストールされているのが確認出来ました。簡単でしたね。

Bower の使い方はこんな感じですが、更に Grunt を繋げることでもうちょっと便利になってます。

Grunt について

Grunt: The JavaScript Task Runner

Grunt は Node.js 上で動作するビルドツールで、タスクを追加して自動的に実行したり出来るみたいです。JavaScript と CSS の結合や Minify といっためんどくさいタスクを簡単に行えるみたいですね。

ASP.NET では System.Web.Optimization というコンポーネントを使って、実行時に JavaScript と CSS の最適化を行えましたが、Grunt を使うことでビルド時に処理を行えるようになります。

Sample Gruntfile - Grunt: The JavaScript Task Runner

Grunt のタスクは Gruntfile.js に定義していくみたいです。サンプルのファイルも用意されているので参考に。

既に JSHint の実行や難読化などを行うタスクが公開されているので、それを使って楽が出来そうです。

Grunt を Visual Studio から使う

Visual Studio 2015 Preview では新しく Task Runner Explorer が追加されて、Grunt の実行を GUI で行えるようになりました。しかし Gruntfile.js は普通に手書きが必要なので注意。

スターターテンプレートを使った場合には、既に bower.json で定義されたパッケージを wwwroot/lib 以下にインストールするためのタスクが追加されてます。

module.exports = function (grunt) {
    grunt.initConfig({
        bower: {
            install: {
                options: {
                    targetDir: "wwwroot/lib",
                    layout: "byComponent",
                    cleanTargetDir: false
                }
            }
        }
    });

    // This command registers the default task which will install bower packages into wwwroot/lib
    grunt.registerTask("default", ["bower:install"]);

    // The following line loads the grunt plugins.
    // This line needs to be at the end of this this file.
    grunt.loadNpmTasks("grunt-bower-task");
};

grunt-bower-task 以外にも自分で好きなタスクを追加することが出来るので、今回は CSS を最適化する grunt-contrib-cssmin というタスクを使ってみます。

まずはインストールが必要なので package.json に grunt-contrib-cssmin を追加します。

IntelliSense が効くので簡単に書けると思います。

ここに追加すれば、ソリューションエクスプローラーの NPM に grunt-contrib-cssmin が not installed という表示で追加されるので、右クリックメニューから Restore Package を選びます。

これでインストールが完了します。今はまだちょっとめんどくさいですが、今後は自動的にインストールされるようになるかもしれないです。

パッケージのインストールが終われば、後は Gruntfile.js にタスクを追加するだけです。

module.exports = function (grunt) {
    grunt.initConfig({
        bower: {
            install: {
                options: {
                    targetDir: "wwwroot/lib",
                    layout: "byComponent",
                    cleanTargetDir: false
                }
            }
        },
        cssmin: {
            compress: {
                files: {
                    "wwwroot/css/site.min.css": [ "wwwroot/css/site.css" ]
                }
            }
        }
    });

    // This command registers the default task which will install bower packages into wwwroot/lib
    grunt.registerTask("default", ["bower:install"]);

    // The following line loads the grunt plugins.
    // This line needs to be at the end of this this file.
    grunt.loadNpmTasks("grunt-bower-task");
    grunt.loadNpmTasks("grunt-contrib-cssmin");
};

とりあえず wwwroot/css の中にある site.css を最適化するように設定しました。

タスクの追加が終われば Task Runner Explorer のツリーに追加したタスクが表示されるので、右クリックメニューから Run を選んで実行してみます。

コンソールが表示されて実行結果が分かるようになってます。CSS の最適化に成功したみたいです。

そして Visual Studio らしい機能として、ビルド前後やプロジェクトを開いたタイミングなどで Grunt のタスクを実行出来るように設定できます。

これで cssmin タスクはビルド後に自動的に実行されるようになります。他にもデプロイ前とかに実行する設定が欲しくなりますね。

ASP.NET 5 では wwwroot が追加されて、クライアントサイドとサーバーサイドのコードが分離されるようになったので、Bower や Grunt などが使いやすくなりましたね。*2

*1:jQuery はともかく Bootstrap は同じようなのがたくさんあったりするし

*2:逆かもしれんけど

自動再生する video タグを古い jQuery で動的に追加すると二重に再生される件

ちょっと実行時に video タグを動的に追加する必要があったので、以下のようなコードを組んでいたのですが、何故か二重に動画が再生されてしまう問題が発生しました。

$("#wrap").append('<video src="kamebuchi.mp4" controls autoplay loop></video>');

ちょっと調べてみると jQuery の parseHTML 実装に原因があったようなので、簡単にまとめておきます。

まずは問題が発生したバージョンと発生しなかったバージョンの parseHTML 実装を調べてみました。

jQuery 1.8.2

// data: string of html
// context (optional): If specified, the fragment will be created in this context, defaults to document
// scripts (optional): If true, will include scripts passed in the html string
parseHTML: function( data, context, scripts ) {
	var parsed;
	if ( !data || typeof data !== "string" ) {
		return null;
	}
	if ( typeof context === "boolean" ) {
		scripts = context;
		context = 0;
	}
	context = context || document;

	// Single tag
	if ( (parsed = rsingleTag.exec( data )) ) {
		return [ context.createElement( parsed[1] ) ];
	}

	parsed = jQuery.buildFragment( [ data ], context, scripts ? null : [] );
	return jQuery.merge( [],
		(parsed.cacheable ? jQuery.clone( parsed.fragment ) : parsed.fragment).childNodes );
},

1.8.2 の実装では buildFragment が cacheable の値として、true を返した場合にはクローンするようになっています。video タグは DOM ツリーに追加されなくても音だけは再生される仕様なので、jQuery にキャッシュされた video タグが音だけ再生していたようです。

jQuery 2.1.1

// data: string of html
// context (optional): If specified, the fragment will be created in this context, defaults to document
// keepScripts (optional): If true, will include scripts passed in the html string
jQuery.parseHTML = function( data, context, keepScripts ) {
	if ( !data || typeof data !== "string" ) {
		return null;
	}
	if ( typeof context === "boolean" ) {
		keepScripts = context;
		context = false;
	}
	context = context || document;

	var parsed = rsingleTag.exec( data ),
		scripts = !keepScripts && [];

	// Single tag
	if ( parsed ) {
		return [ context.createElement( parsed[1] ) ];
	}

	parsed = jQuery.buildFragment( [ data ], context, scripts );

	if ( scripts && scripts.length ) {
		jQuery( scripts ).remove();
	}

	return jQuery.merge( [], parsed.childNodes );
};

ちなみに 2.1.1 の実装ではパースした結果をキャッシュする仕組みは無くなっています。

このように実装が変わったのは 1.9.0 からのようなので、新しい jQuery を使っている限りでは、二重に再生される問題は発生しないようになりました。どうしても新しい jQuery にアップデート出来ない場合には、createElement を使って昔ながらの方法で要素を作るしかなさそうです。

var video = document.createElement("video");

video.src = "kamebuchi.mp4";
video.controls = true;
video.autoplay = true;
video.loop = true;

$("#wrap").append(video);

jQuery の parseHTML を通さないので、要素がキャッシュされず問題なく再生されるようになりました。

ASP.NET 開発者以外にもおススメしたい Microsoft Ajax CDN

Microsoft は Windows Azure などで利用するために、日本国内に CDN のエッジサーバを用意しています。その CDN エッジサーバを利用して jQuery などの有名な JavaScript ライブラリを配信する、Microsoft Ajax CDN というサービスを提供しています。

Microsoft Ajax Content Delivery Network - ASP.NET Ajax Library

使い方は非常に簡単で、普通に script タグで提供されている URL を読み込むようにするだけです。

<!-- jQuery 2.0.3 を読み込む -->
<script src="//ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.3.min.js"></script>

スキームを省略しているのは https の時に対応するためです。ちゃんと圧縮とキャッシュも有効になっているので、ネットワークアクセスでのオーバーヘッドが最低限に抑えられています。

同じようなサービスとして Google が提供している Google Hosted Libraries があり、世間的にはこちらの方がよく使われている印象ですね。

Make the Web Faster — Google Developers

双方のサービスで配信されているライブラリを表にまとめてみました。

Microsoft Ajax CDN Google Hosted Libraries
jQuery
jQuery Migrate
jQuery UI
jQuery Validation
jQuery Mobile
jQuery Templates
jQuery Cycle
jQuery DataTables
Modernizr
Ajax Control Toolkit
JSHint
Knockout
Globalize
Respond
Bootstrap
AngularJS
ChromeFrame
Dojo
ExtCore
jQuery
jQueryUI
MooTools
Prototype
script.aculo.us
SWFObject
WebFontLoader

jQuery プラグインが Microsoft Ajax CDN の方が充実しているのに対して、JavaScript フレームワークが充実しているのが Google Hosted Libraries ということになりますね。配信されているライブラリには差がありますが、jQuery と jQuery UI は双方で配信されているので、好きな方を選んで使うことができます。

でも、私の環境から Microsoft Ajax CDN と Google Hosted Libraries のレイテンシを確認すると、Microsoft Ajax CDN の方が短い傾向がありました。

まずは Microsoft Ajax CDN の エッジサーバへ向けて ping を打った結果です。

PS C:\Users\shibayan> ping ajax.aspnetcdn.com

mscomajax.vo.msecnd.net [65.54.91.121]に ping を送信しています 32 バイトのデータ:
65.54.91.121 からの応答: バイト数 =32 時間 =18ms TTL=54
65.54.91.121 からの応答: バイト数 =32 時間 =21ms TTL=54
65.54.91.121 からの応答: バイト数 =32 時間 =20ms TTL=54
65.54.91.121 からの応答: バイト数 =32 時間 =27ms TTL=54

65.54.91.121 の ping 統計:
    パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 18ms、最大 = 27ms、平均 = 21ms

次に Google Hosted Libraries のエッジサーバへ向けて ping を打った結果です。

PS C:\Users\shibayan> ping ajax.googleapis.com

googleapis.l.google.com [74.125.23.95]に ping を送信しています 32 バイトのデータ:
74.125.23.95 からの応答: バイト数 =32 時間 =55ms TTL=45
74.125.23.95 からの応答: バイト数 =32 時間 =55ms TTL=45
74.125.23.95 からの応答: バイト数 =32 時間 =77ms TTL=45
74.125.23.95 からの応答: バイト数 =32 時間 =54ms TTL=45

74.125.23.95 の ping 統計:
    パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 54ms、最大 = 77ms、平均 = 60ms

tracert を実行したところ、Microsoft Ajax CDN の方がホップ数が少なかったです。環境に左右されるとは思いますが、Microsoft Ajax CDN も選択肢の一つに入れても良いのではないかと思います。

なお、Microsoft Ajax CDN と Google Hosted Libraries に共通の事項として、何らかの問題が発生して CDN からの読み込みが失敗した時のために、フォールバック用のコードを仕込んでおいた方が安全でしょう。

<script src="//ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.3.min.js"></script>
<script>window.jQuery || document.write('<script src="js/jquery-2.0.3.min.js"></script>')</script>

Scott Hanselman 氏のブログでは ASP.NET の ScriptManager や Bundle を使った方法も紹介されています。

CDNs fail, but your scripts don't have to - fallback from CDN to local jQuery - Scott Hanselman

Windows Azure の日本リージョンも来年には稼働が開始しますし、日本にエッジサーバが増えて帯域も広くなる可能性もあるので期待したいですね。

ASP.NET MVC 4 のブラウザオーバーライドを JavaScript から設定する

ASP.NET MVC 4 というか Web Pages 2 では、ブラウザのオーバーライド機能が追加されたことはみんな知っていると思います。知らなかった人は以下のブログ記事を 3 回ぐらい読んでおいてください。

Making a switchable Desktop and Mobile site with ASP.NET MVC 4 and jQuery Mobile - Scott HanselmanASP.NET MVC 4 : モバイル デバイスの検出とビューの切り替え機能 (2) - THE TRUTH IS OUT THERE - Site Home - MSDN Blogs

同じく ASP.NET MVC 4 というか Web Pages 2 で追加された Display Modes という機能を使って実装されてるんですが、よくサンプルで出てくる jQuery.Mobile.MVC では切り替え用のアクションに遷移して、リダイレクトで戻ってくるような実装になってます。

実際のところ、このブラウザのオーバーライド機能では、デフォルトではクッキーに .ASPXBrowserOverride という名前で User-Agent 文字列が保存されているだけなので、JavaScript だけで完結できそうですよね。*1

という訳で試しました。

// PC 版に切り替えるための UA
var ua = 'Mozilla/4.0 (compatible; MSIE 6.1; Windows XP)';

// クッキーの有効期限は標準と同じく 7 日間
var expires = new Date();
expires.setDate(expires.getDate() + 7);

// クッキーを書き込む
document.cookie = '.ASPXBrowserOverride=' + encodeURIComponent(ua) + '; expires=' + expires.toUTCString() + '; path=/';

// ページを再読み込み
location.reload();

通常はスマホ版のページから PC 版のページに切り替えるケースだと思うので、UA を PC 向けのものにしています。当たり前ですがリロードしないと反映されないので注意です。

元に戻したい場合にはクッキーを削除すればいいです。

// 有効期限を過去にする
document.cookie = '.ASPXBrowserOverride=; expires=' + new Date(0).toUTCString() + '; path=/';

// ページを再読み込み
location.reload();

専用のコントローラを用意する必要がなく、さらにリダイレクトもさせることなく切り替えが出来るのは、無駄なリクエストの削減の観点からも良いと思います。

追記

上記の例ではクッキーの操作に直接 DOM を触っていましたが、jQuery Cookie という素晴らしいプラグインがあったので、こっちを使うように修正してみました。

var ua = 'Mozilla/4.0 (compatible; MSIE 6.1; Windows XP)';

$.cookie(".ASPXBrowserOverride", ua, { expires: 7, path: '/' });
            
location.reload();

そしてクッキーの削除もメソッドが用意されているので簡単です。

var cookie = $.cookie(".ASPXBrowserOverride");
                
if (cookie) {
    $.removeCookie(".ASPXBrowserOverride");
                    
    location.reload();
}

クッキーの存在確認が簡単にできるので、スマホ版に戻すボタンの表示処理まで JavaScript で簡単に完結することができますね。

*1:System.Web.WebPages の CookieBrowserOverrideStore クラスを参照

Tweet Button の独自イベントを取得してみる

Tweet Button はイベントを登録しておくと、ツイートされた時にコールバック関数を実行することが出来るようになっています。この機能は結構便利そうですよね。

Web Intents Javascript Events | Twitter Developers

注意点としては、標準の widget.js を読み込むタグではなくて、このページに書いてある専用のタグを使う必要があります。具体的には以下のようなやつです。

<script type="text/javascript" charset="utf-8">
  window.twttr = (function (d,s,id) {
    var t, js, fjs = d.getElementsByTagName(s)[0];
    if (d.getElementById(id)) return; js=d.createElement(s); js.id=id;
    js.src="//platform.twitter.com/widgets.js"; fjs.parentNode.insertBefore(js, fjs);
    return window.twttr || (t = { _e: [], ready: function(f){ t._e.push(f) } });
  }(document, "script", "twitter-wjs"));
</script>

機能自体は同じなんですが、このタグを使わないと twttr というグローバルな変数が定義されないので、イベントの登録が出来ません。

そして widget.js は最近のソーシャル系のボタンと同様に非同期で読み込まれるので、そのために twttr.ready メソッドを使って読み込み完了まで待つ必要があります。

twttr.ready(function (twttr) {
    twttr.events.bind("tweet", function (event) {
        // ツイートされたタイミングで呼び出される
    });
});

他にもイベントとしてはフォローしたタイミングや、ボタン自体がクリックされたタイミングで呼び出されるものもあります。

// フォローしたユーザー情報を取得する
twttr.events.bind("follow", function (event) {
    var followed_user_id = event.data.user_id;
    var followed_screen_name = event.data.screen_name;
});

// クリックされた場所を取得する
twttr.events.bind("click", function (event) {
    if (event.region == "count") {
        // ツイート数表示がクリックされた
    }
});

今回はこの機能を使って、だるやなぎメーカーで新しいだるやなぎを作ってツイートした時にトースト通知を表示するようにしました。

だるやなぎメーカー - あなただけの、だるやなぎを簡単に

ちなみにトースト表示は SignalR を使ってページを開いている全員に配信されるようになっています。

Web アプリでお手軽にトースト通知を行ってみる

Dev Intersection の何処かのセッションで toastr という簡単に使えるトースト通知のライブラリが紹介されていて、これが結構面白くて便利そうだったので試しました。

NuGet Gallery | toastr 1.2.2

NuGet 経由で簡単にインストールできるのが相変わらず便利ですね。簡単な使い方は以下の通りです。

// 「情報」トーストを表示
toastr.info("はうはう", "タイトル");

// 「成功」トーストを表示、クリックイベント付き
toastr.success("はうはう", "タイトル", {
    onclick: function () { alert("click"); }
});

実行すると、こんな感じで表示されます。

f:id:shiba-yan:20130418051701p:plain

もっといろいろと試したい場合は、デモ用のページが用意されているのでこっちでいろいろ試すのが良さそうですね。

toastr examples

ちなみに作者は ASP.NET の Single Page Application で有名な John Papa 氏です。

Simple JavaScript Notifications with toastr | John Papa

Dev Intersection は Microsoft 社外の人のセッションも多かったので、グローバルサミットとは違う楽しみがありました。

JavaScript だけで画像をピクセルデータに変換する

年明けにいろいろやっていた時に JavaScript で画像加工が面白かったので試し続けてます。

基本は画像を読み込まないといけないので、まずは画像を読み込んでピクセルデータに変換する処理を書いてみました。Deferred を使っているので jQuery が必要です。

function loadImageAsync(path) {
    var deferred = $.Deferred();

    var image = new Image();

    image.onload = function () {
        var width = this.width;
        var height = this.height;

        var canvas = document.createElement("canvas");

        canvas.setAttribute("width", width);
        canvas.setAttribute("height", height);

        var context = canvas.getContext("2d");

        context.drawImage(this, 0, 0);

        var imageData = context.getImageData(0, 0, width, height);

        deferred.resolve(imageData);
    };

    image.onerror = function () {
        deferred.reject();
    };

    image.src = path;

    return deferred.promise();
}

基本的な方針としては Canvas を使ってピクセルデータを取得しています。画像の読み込みが非同期なので Deferred を使って使いやすくしました。

実際に使う場合には以下のように書きます。

loadImageAsync("sample.png").done(function (imageData) {
    // imageData.data に RGBA で色データが入っている
});

複数の画像を読み込んでから処理をしたい場合には Deferred なので $.when を使ってあげるだけですね。

$.when(loadImageAsync("first.png"), loadImageAsync("second.png"), loadImageAsync("third.png")).done(function (first, second, third) {
    // 何かする
});

いろんな画像サイズに対応するために Canvas を毎回生成していますが、思ったほど重くなさそうな感じです。