ブログを移転しました!
ネコメシが社員ブログを独立して始めたことに伴い、本ブログ、ダーシマ・ヱンヂニヤリングも移転しました。
超絶おすすめブログです!!
記事の移行はないため、このはてなブログはそのまま残していますが、フィード購読などの登録をされていた場合は登録変更をお願いします。
今後とも、ネコメシぶろぐ及びダーシマ・ヱンヂニヤリングを宜しくお願い申し上げます。
実践 めんどうくさくない BEM
この記事は BEM Advent Calendar 2013 の12日めの記事です。
BEM は優れた方法論だと思うが、大変めんどうくさいことを強いてくることがある。この記事ではそんなめんどうくさい BEM を、少しでもめんどうくさくない BEM に変えられるかどうかを思索するものである。なお、めんどうくさくなくする過程で「それは既に BEM ではな」くなっている面もあると思うが、そこは承知の上なので念頭に置かれたし。
続きを読むJavaScript でツイート文字列の文字数を正しくカウントする
2015/09/11 Gistのライブラリをアップデートしました。
2014/10/13 Gistのライブラリをアップデートしました。
JavaScript 経由で Twitter に投稿する処理を持つウェブアプリはよくあると思う。そのときに実装する必要がある機能のひとつに文字数カウントの機能がある。
Twitter API に投稿リクエストをすれば、入力したツイートが文字数オーバーしていたかどうかはわかるけれど、ユーザー導線を考えて JavaScript 内でバリデーションを行いたい機会は少なくないはずだ。
はじめにお断りしておくと、この記事はあくまでもツイート文字列の文字数を * 正しく * 数えたい神経質な人向けなので、ある程度の精度でよいという方は他の方の作られたライブラリを参照するのがよいと思います。
ツイート文字数カウントのセオリー
ふつうに、 $('#tweet').val().length
で良いじゃんと思いがちだが、文字列を正規化したり、 URL を短縮したりする処理のことを考えると一筋縄には行かない。現行の Twitter では URL は必ず短縮されることになっているため、どんなに長い URL であろうとも、 HTTP の URL なら 22 文字、 HTTPS の URL なら 23 文字としてカウントされることになっている。 23文字としてカウントされることになっている。
URL を正しく正規表現で引っ掛けるのはなかなか骨が折れる。しかし考えてみれば、ツイート文字数のカウントはウェブ版 Twitter アプリで使われているんだから、正確にカウントしたければウェブ版 Twitter で使われているロジックをそのまま流用できればよい。
都合のよいことに、 Twitter がウェブ版アプリで使っているテキスト処理は外部化されていて、 GitHub にリポジトリが公開されている。
上記 URL にアクセスした先にある twitter-text.js のリンクをクリックすると、そのソースが出てくる。これを流用すれば、正確なツイート文字数カウントができるはずだ。ためしに実装してみたのが以下。 twttr.txt.getTweetLength というメソッドが文字数を数える関数で、プレーン文字列を渡せば文字数が返ってくる。
自分のウェブサイトで使う場合は、 twitter-text.js をダウンロードし、ページから読みこめばよい。
文字数を数えたいだけなのにライブラリが巨大問題
twitter-text.js は、現時点でなんと 1328 行もある。正確に数えるためとはいえ、ツイート文字数を数えるためだけにこのライブラリを導入するのはちょっと気後れする。
このライブラリは、文字数を数える以外にもいろいろな機能が含まれている。ツイートが投稿可能な正当なものであるかどうかを判別する関数であるとか、 @user やハッシュタグが含まれているツイートにリンクタグを付与する関数など。これらの機能は、文字数を数えるという目的には必要ないので削ることができる。
そこで削ってみたのが以下。
ツイートの文字数カウント (わたしの Gist に飛びます)
まだ 350 行あるが、だいぶましになった。
Zepto.js の $.ajax にオブジェクトの配列を渡した時の不具合を直す
Zepto.js は jQuery と同じように使えるように互換性を保ちつつ、機能削減や軽量化・高速化が図られているライブラリ。ホワイトスペースなどを除去すれば 20kB 強くらいのサイズになるので、読み込み容量がシビアなモバイル向けのウェブサイトを作るときには特に重宝する。
Zepto は基本的には jQuery と互換性があるが、ときどき微妙なところで挙動が異なっているのが玉にきず。 Ajax リクエストを行うための関数である $.ajax もそんな傷入りの機能のひとつのようだ。
今回取り扱うのは、 $.ajax のリクエストパラメーターとしてオブジェクトの配列を渡した時の挙動が、 Zepto と jQuery とで異なっている不具合。ちなみに v1.0 時点での不具合なので、もっとあとのバージョンでは修正されているかもしれない。
jQuery
Zepto.js
パラメータにオブジェクトの配列を渡した時に、出力される文字列が異なっているのがわかる。
上のコードにも書かれているけど、 $.ajax の内部では $.param という関数が呼ばれている。この関数はオブジェクトを受け取って、 Ajax リクエストを行うための形式に変換する関数で、まさに挙動の差の原因となっている部分。 Zepto.js の吐き出し方は pAryObj[][a]=bc
と pAryObj[][a]=gh
がそれぞれ何番目のオブジェクトに属するプロパティなのか判別しようがないので、 PHP でパラメーターを受け取るとおかしくなる(と思う。未検証だけど)。ここは jQuery の出力形式に統一したい。
挙動の差を埋める
以下のコードを Zepto.js の当該コードと置き換えればよろしい。Zepto.js v1.0 のコードを置換することを想定しているので、それ以外のバージョンの場合はお気をつけて。
function serialize(params, obj, traditional, scope){ var type, objIsArray = $.isArray(obj), objIsObject = $.isPlainObject(obj) $.each(obj, function(key, value) { type = $.type(value) if (scope) key = traditional ? scope : scope + '[' + (objIsObject || type == 'object' || type == 'array' ? key : '') + ']' // handle data in serializeArray() format if (!scope && objIsArray) params.add(value.name, value.value) // recurse into nested objects else if (type == "array" || (!traditional && type == "object")) serialize(params, value, traditional, key) else params.add(key, value) }) } $.param = function(obj, traditional){ var params = [] params.add = function(k, v){ this.push(escape(k) + '=' + escape(v)) } serialize(params, obj, traditional) return params.join('&').replace(/%20/g, '+') }
上記のコードは、 Zepto.js の GitHub リポジトリに上がっていたプルリクエストから抜粋した。6ヶ月前に起票されたプルリクだけれども今のところ取り込まれていない。しかし Zepto.js のコミッターの人も口出ししているので、そのうち取り込まれるのではないかと思う。
多くの人は、ライブラリの JS ファイルに直接手を入れるのは気が進まないと思う。その場合は以下のような感じでムリヤリオーバーライドする感じでいいのではないかと。
RequireJSをプロジェクトで使ってみての所感
JavaScriptのモジュール定義・読み込みをAMD (Asynchronous Module Definition) でできるライブラリであるRequireJSをプロジェクトで使ってみた。
プロジェクトの規模感を簡単に説明しておくと、まず単画面アプリケーションではなく、従来通りテンプレートが複数あるウェブサイトだ。よくあるウェブサイトとはいえ、JavaScriptで動く機能やUIがたくさんあり、テンプレートのパターンが100枚以上ある。カルーセルと画像切り替えが動けばヨシ、みたいなそこらのコーポレートサイトよりは遥かに規模の大きいウェブサイトとなっている。
導入を決めたのは、今回のプロジェクトで必ず現出するであろう懸念点を、あらかじめ潰しておきたかったという動機から。その懸念点というのは主にファイルサイズの問題。ウェブサイト内で使われている全JSファイルを全て結合圧縮すると、最終的に1MBくらいになってしまい、初回ロード時に読ませるにはちょっとキツい。可能なら、使用されるページが限定されているJSファイルは、必要なときにだけリクエストされるようにしたかった。
そういう意味では、今回の導入の動機は“ファイルサイズ、リクエスト数の最適化”であって、“モジュール管理”ではない。という前提がありますね。
RequireJSを使って良かった点
- まともにモジュール管理ができるようになった
- コンパイルが強力。Gruntとの相性が良かった
- 各ページで読み込まれるJSファイルを3ファイル以内に抑えられた
RequireJSを使ってイマイチだった点
- scriptタグをHTMLに埋め込むと厄介
- 稀になかなかJSが実行されない
以下、詳細。
まともにモジュール管理ができるようになった
JavaScriptは言語仕様としてモジュールの仕組みを持っていないため、モジュール管理的な事をしようと思ったら自前で用意しなければならない。たいていの場合、ルールとしてモジュール名やオブジェクトのツリー構造を決めていくことになる。RequireJSを導入することによって、1ファイル=1モジュールと半ば強制的になる。そのためツリー構造はフォルダツリーを見れば把握できるようになる。
それから、依存関係を明示することで、そのモジュールの正体が把握しやすくなった。JSの読み込み順を気にする必要もなく、無用なストレスから開放された。
コンパイルが強力。Gruntとの相性が良かった
RequireJSを使うと、1モジュール=1ファイルになるため、ファイル数がドーンと増える。あわせてリクエスト数もドーンと増えてしまうのでは? という懸念があるが、提供されているオプティマイザーを使うと、依存しているファイルを再帰的に探って1ファイルにまとめてくれる。このオプティマイズのタスクをGruntに任せることができて、楽だった。
「全部のページでモジュールA,B,Cは読み込ませたい。モジュールD,Eは特定のページでしか使わない」という場合にも、オプティマイザを使うことで「モジュールA,B,Cを含むABC.js」と「モジュールD,Eを含むDE.js」に別々に結合・圧縮することもできる。モジュールDがモジュールAに依存していたとしても、DE.jsにはAを含まないように、よしなにやってくれたりもする。
このやり方はまた別のエントリーにするかもしれない。
各ページで読み込まれるJSファイルを3ファイル以内に抑えられた
前項のコンパイルを施すことで、①全ページ共通のJS、②画面固有のJS、③RequireJS本体 の3ファイルにまとまった。
scriptタグをHTMLに埋め込むと厄介
RequireJSに則ってモジュール定義したものを、HTMLの中から直接script
タグを使って埋め込もうとするとハマる。非同期読み込みなので、script
タグが登場した時点でそのモジュールが存在している保証がないためだ。
そもそもどういう機会にscript
タグをHTMLに直接記述するかというと、例えば画像切り替えのJSを即時発動したい時。よくある書き方は以下のような感じ。
$(function () { $('.image_switcher').imageSwitcher(); })
シンプルでわかりやすいな発動方法だが、DOM Readyのタイミングまで発動されないのが欠点。そのため、画像をクリックしても何も起きないタイミングが存在してしまうことになる。そこで、script
タグを使って記述すると、
<div class="image_switcher" id="image_switcher_1"> <!-- 画像が何枚か並んでる --> </div> <script> $('#image_switcher_1').imageSwitcher(); </script>
となり、該当箇所のDOM構築が終わった直後にJSを発動するので、画像をクリックしても何も起きないタイミングがほぼ無い。
RequireJSを使用していると、上の例でscript
タグが登場した時点でそのモジュールが存在している保証がないため、こういう細かいパフォーマンス調整は出来なくなってしまう……。
稀になかなかJSが実行されない
予想以上にスクリプトのリクエストが遅延して、なかなかJSが発動されないケースがある。ブラウザにもよるけど、ブラウザは同時に8リクエストくらいしかできない。その帯域をCSSや画像などで埋められていると、JSのリクエストが後回しになり、そのぶん実行が遅れる。結果的にJSの実行がDom Readyよりずっと後になってしまうこともある。
総評
少なくとも、 RequireJS は、 Sass のような「とりあえず入れとけばよい」系のプロダクトではないと思った。入れるべきか、控えるべきかを切り分けるキーになるものは、プロジェクトの規模なのかなー。ある程度の規模になると、モジュール管理の機構をなくしてコードを書くのが困難になる。スクリプトの結合・圧縮もスマートに行いたくなる。このあたり、必要のない程度の規模のプロジェクトであれば、わざわざ導入しなくてもよいかもしれない。
特に、JSの実行が大幅に遅れてしまうことが稀にある問題は、結構なネックとなっているとおもう。仕組み的に仕方ないとはいえ、これがあるから導入を見送るという考え方は全然アリだと思う。