SafariのUA文字列が固定されて固定されなくなったおはなし

Jxckが以前、SafariUA文字列が固定されたというのを書いていた。

Safari Technology Preview 46で入った変更だ。

Froze the user-agent string to reduce web compatibility risk and to prevent its use for fingerprinting

TwitterでもAppleRicky Mondelloがそれを伝えていて、ちょっと騒ぎになっていた。「それは困る」的な反応が結構多かったのと、中には「やっぱりSafariは新たなIE6だ」みたいな、リスペクトのない反応もちらほら。なんだかね……


さて、これだけだったらここで書くことはなにもないんだけど、続きがある。その後この固定化はとりやめられたのだった。

現在はUA文字列に含まれていたOSのバージョンが固定されず、ちゃんと実際のバージョンを反映するようになっている。先日リリースされたSafari 11.1でもUA文字列の変更については述べているものの、固定という表現ではなくなった。

Updated the User-Agent String Policy

  • Changed the updating policy to to only continue updating the OS version number and Safari version number.

参考までに、手元のmacOS 10.13.4で確認した、Safari 11.1のUA文字列を。

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15 

以下はUA文字列固定の経緯と、再度変更されるまでの流れ。

UA文字列の固定とはなんだったのか

RickyのツイートやSTP 46のリリースノートだけだと詳細がわからないが、ちゃんとコミットがある。

Freeze the version reported as User Agent to OS 10.13.4 (OS 11.3 on iOS) and WebKit 605.1.15 for User Agent purposes.

macOSのバージョンが10.13(High Sierra)以降の場合は10_13_4iOSの場合は11_3になると。ついでにWebKitのバージョンも605.1.15に固定される。

なので、パッチが入ったSTP 46からしばらくは、以下のようになっていた。

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15

STP 46の時点ではmacOSは10.13.3なので、たしかにOSのバージョンが固定されている感じなのがわかる。

変更の理由

STPのリリースノートにもフィンガープリンティングを避けるという目的が書いてあったけど、該当バグにもう少し細かく書かれていた。

Stop reporting more recent user agent version strings, freezing the version at a specific moment in time. We should do this for a number of reasons:

(1) User Agent sniffing is a terrible way to determine whether a browser supports certain features.

(2) Bad User Agent sniffing code on the web create compatibility problems every time we update the versions.

(3) Overly-specific version information provides useful fingerprinting data while providing almost no user benefit.

  1. ある機能のサポート状況をチェックする方法としてひどい
  2. UA文字列検出のコードがひどいと互換性の問題にバージョンを上げるたびに毎回出くわす
  3. 細かすぎるバージョンの情報はフィンガープリンティングにつながる危険性がある

フィンガープリンティングだけではなくて、UA文字列について一般的に問題とされていることも理由となっていた。

OSのバージョン固定がなくなった

Rickyのツイートにもたくさんの「困る」という声があったし、Appleにそのような声が寄せられたか、はたまた互換性のバグに引っかかったのかわからないけど、件のコミットから2ヶ月ほど経った今年2月、方針が転換されるコミットが入った。

Revert back to dynamically reading the operating system marketing version rather than using a hard-coded version.

OSのバージョンをシステムから読むように(再度)変更したと。つまりは固定されなくなった。

いったん固定するとした方針を軟化させるにあたっては、バグに議論があった。

まずは固定をやめる理由について。

The primary reasons were:

  1. WebKit sometimes ships with bugs that can be worked around at the website level. Having a release version allows websites to activate workarounds where needed.

  2. Decisions about what site content to send to a user agent are often made before the page is loaded. Having UA information when connecting to the server allows websites to send only the relevant payload (e.g., more recent javascript, more efficient image formats, etc.)

  3. Safari (for example) ships with the same version on several operating system revisions. Sometimes bugs exist in specific OS revisions that websites can work around.

For these reasons, we decided to relax some of the restrictions we had originally planned on making.

  1. バグがある状態でリリースされてしまうと、ウェブサイトで対応するしかない。リリースバージョンを含めるを含めることでワークアラウンドが可能になる
  2. ブラウザごとにコンテンツを出し分けるサーバーサイドのコードがあって、UA文字列を含めることで必要なものだけを送れる
  3. SafariはOSといっしょにアップデートされるけど、OSにバグがあるときのワークアラウンドに使える

といった感じ。1、2についてはどのブラウザにも言えるだろうけど、3については半年に一度のリリースで、かつOSとくっついているSafariならではの事情が大きそうだ。

一旦は決めた固定をやめること、そしてその理由については、それはどうなんだという反発もあった。mitz氏はこう問うている。

Shouldn’t WebKit provide a direct and explicit way for websites to determine that a bug is fixed?

バグが直ったら伝える仕組みを設けるべきではという。ドキュメンテーションよりも直接的なものを想定しているのかな。とはいえ、だいぶ理想を見ているようにも見える。そういうのがあってうまく機能したなら、世の中はXMLベースになっていたんじゃないかな。

UA文字列という手段を使い続けることについても、別の方法がないのかという疑問も提示している。

Aren’t there other request headers dedicated to explicitly, directly conveying what the user agent supports?

Client Hintsみたいなものの機能版という感じかな。ヘッダではないけどhasFeature()というものがあって、これはなかなか残念な結果だったはずなので、ヘッダにあってもあまりうまく機能しない気がする。「サポートしている」の粒度が人によってそれぞれ違うだろうから、うまく満たせる仕組みはつくれないだろう。

とまあ、疑問が投げられたけど結局コミットされたので、使えるUA文字列が復活した。めでたいのか、めでたくないのか。


なお、OSのバージョンは固定されなくなったが、WebKitのバージョンは605.1.15に固定されたままだ。WebKitのバージョンでなにかしたいケースってあるかな……ほしいのはOSバージョンだろうから、こいつはそこまで気にされないかなーと思う。

おまけ:なんでアンダースコアで区切ってるのか

OSのバージョンはなぜだか10_13_4と、アンダースコア区切りになっている。

なんかあったのかなーと思ったら、UserAgentCocoa.mmというファイルにこんなコメントが書いてあった。

// Use underscores instead of dots because when we first added the Mac OS X version to the user agent string
// we were concerned about old DHTML libraries interpreting "4." as Netscape 4. That's no longer a concern for us
// but we're sticking with the underscores for compatibility with the format used by older versions of Safari.

OSのバージョンをUA文字列にぶっこんだときに、4.という文字列が現れたら問答無用でNetscape 4と認識するDHTMLライブラリに出会ったらしくそれを迂回する目的だったらしい。たしかにNetscape Communicator時代は4.0とか4.5とか4.7とかマイナーバージョンが更新されたから、4.でマッチさせるようにしたのかな。たしかにNetscapeはいまと比べてUA文字列だいぶ短かったし。

うーんでも、4.でマッチするならIEもマッチする気がするんだけどな……

コミットを探したらあった。

Added Mac OS X version string, right after the string "Mac OS X", but with underscores instead of dots to avoid the dreaded "4." problem (old libraries that think a "4." anywhere in the user agent means Netscape 4).

No way to detect Tiger vs Leopard from Safari's user agent stringと、TigerLeopardを判別したいなにかがあったらしい。なのでOSのバージョンを入れたのだけど、4.が問題になった過去の問題をふまえてアンダースコアにしたと。

ではその4.についての言及はどれかなーとさらに探したら、これまたあった。ちゃんとchangelog書いてるのすごいなー

Adds a build phase script that ensures WebKit's version dosen't end in a 4. If our version ends in 4, some sites might think we are Netscape 4 in their user agent checks.

これを読むと4.ではなく4な気がするんだけど、いつのまにか4.になってるのはどうしてなんだろう。もうわかることはないんだろうけど。

元のコメントを読むと、現在はドットにしたところで互換性の問題はないらしいが、逆に過去のSafariとの互換性がとれなくなるという話でアンダースコアが残されているらしい。ふしぎなコードとバグと互換性でウェブは続いていく。

Chromeでボタンをクリックしたときのフォーカスリング

geckotangが「Chromeでボタンをマウスでクリックしたときにフォーカスリングが出たり出なかったりするのなんで」と言っていて、なんかお前調べろよ的な感じだったので調べることにした。

使うのはcs.chromium.org<button>なので"HTMLButtonElement"で検索して、それっぽいファイルを探す。あった。

"focus"で検索するとSupportsAutoFocus()というメソッドが見つかる……がこれはautofocus属性の対象になるかとかそういう感じがするのでこれではない。というかそっか、autofocus指定できるんだ……<input>にしか使ったことなかったからなんか使えないって思ってしまってた。

HTMLButtonElementにはなさそうなので、参照しているHTMLFormControlElementを見る。

"focus"で検索……ShouldShowFocusRingOnMouseFocus()とかShouldHaveFocusAppearance()とか、それっぽいのがある!

メソッドを選択して、Call Hierarchyを見てみる。layout_theme.ccShouldDrawDefaultFocusRing()というメソッドが見える。これかーーーー!?

フォーカスリングが出る条件

ShouldDrawDefaultFocusRing()はこうなっていた。

bool LayoutTheme::ShouldDrawDefaultFocusRing(const Node* node,
                                             const ComputedStyle& style) const {
  if (ThemeDrawsFocusRing(style))
    return false;
  if (!node)
    return true;
  if (!style.HasAppearance() && !node->IsLink())
    return true;
  // We can't use LayoutTheme::isFocused because outline:auto might be
  // specified to non-:focus rulesets.
  if (node->IsFocused() && !node->ShouldHaveFocusAppearance())
    return false;
  return true;
}

if文を読み解く。

  1. テーマがフォーカスリングを出す → false
  2. ノードじゃない → true
  3. アピアランスががない、かつリンクでない → true
  4. フォーカスされている、かつフォーカスを表示すべきじゃないと判断してる → false

ふんふん(わかってない)

テーマのフォーカスリング

テーマがフォーカスリングを出すのかは、ThemeDrawsFocusRing()を見ればいいらしい。メソッドをクリックするとlayout_theme.hに飛んだ。

  virtual bool ThemeDrawsFocusRing(const ComputedStyle&) const = 0;

さらにクリック。layout_theme_default.ccなるものに飛ぶ。

bool LayoutThemeDefault::ThemeDrawsFocusRing(const ComputedStyle& style) const {
  if (UseMockTheme()) {
    // Don't use focus rings for buttons when mocking controls.
    return style.Appearance() == kButtonPart ||
           style.Appearance() == kPushButtonPart ||
           style.Appearance() == kSquareButtonPart;
  }

  // This causes Blink to draw the focus rings for us.
  return false;
}

基本的にはfalseらしい。trueになりそうなのもテストで使う系なのかな。

プラットフォームによって上書きされてるかもしれないなーと思ったけど、layout_theme.ccの上の方にThe methods in this file are shared by all themes on every platform.なんてコメントがあった。しかしこのメソッド、テスト以外になにか使い所あるのかなあ。

アピアランスがあるかないか

アピアランスがない、というのは-webkit-appearanceプロパティの話らしい。computed_style.hHasAppearance()が定義されていた。

  // -webkit-appearance utility functions.
  bool HasAppearance() const { return Appearance() != kNoControlPart; }

kNoControlPartとは、theme_types.hのControlPartというenumに定義されていた-webkit-appearance: noneに相当する定数っぽい。

CSSProperties.json5(CSSプロパティの定義を生成するもとのファイル)を見ると、-webkit-appearanceの初期値がkNoControlPartと定義されてたので、認識はあってそう。

フォーカスを見せるのか

フォーカスすべきでないという判断は、ShouldHaveFocusAppearance()を見ればいい。クリックしたらhtml_form_control_element.ccに定義されていた。

bool HTMLFormControlElement::ShouldHaveFocusAppearance() const {
  return !was_focused_by_mouse_ || ShouldShowFocusRingOnMouseFocus();
}

ShouldShowFocusRingOnMouseFocus()はすぐ上にあった

bool HTMLFormControlElement::ShouldShowFocusRingOnMouseFocus() const {
  return false;
}

マウスでフォーカスされていない場合にのみ、フォーカスすべきという話らしい。


以上をふまえると、基本的にbutton要素は以下の場合にフォーカスリングを出すらしい。

  • -webkit-appearanceがない場合
  • マウス以外でフォーカスした場合

なので、通常のボタンをマウスでクリックした場合はフォーカスリング出ない。

appearanceプロパティのcomputed styleが変わるとき

geckotangによるとborderやらbackground-colorなどを指定すると、マウスクリック時にもフォーカスリングが出るとのこと。

先程のShouldDrawDefaultFocusRing()trueになる条件をふまえると、-webkit-appearanceがない場合にフォーカスリングが出るということなので、-webkit-appearanceが変わる条件を探せばよい。

さてどうしたもんかなーと思っていたのだけど、layout_theme.ccの上の方にこんなコードがあった。

void LayoutTheme::AdjustStyle(ComputedStyle& style, Element* e) {
  DCHECK(style.HasAppearance());

  ControlPart part = style.Appearance();

  // ...

  if (IsControlStyled(style)) {
    if (part == kMenulistPart) {
      style.SetAppearance(kMenulistButtonPart);
      part = kMenulistButtonPart;
    } else {
      style.SetAppearance(kNoControlPart);
      return;
    }
  }

  // ...

}

アピアランスを見て、特定のアピアランスでない場合はアピアランスを変更するというコード。

これどこで使われるのかなーと思ったのだけど、layout_theme.hに関数の説明が書いてあった

  // This method is called whenever style has been computed for an element and
  // the appearance property has been set to a value other than "none".
  // The theme should map in all of the appropriate metrics and defaults given
  // the contents of the style. This includes sophisticated operations like
  // selection of control size based off the font, the disabling of appearance
  // when certain other properties like "border" are set, or if the appearance
  // is not supported by the theme.
  void AdjustStyle(ComputedStyle&, Element*);

スタイルが計算された、もしくはappearancenone以外になったときにコールされると。おっけ。これだ。

どんなときにアピアランスが変わるのか

もっかいさっきのコードを。

  if (IsControlStyled(style)) {
    if (part == kMenulistPart) {
      style.SetAppearance(kMenulistButtonPart);
      part = kMenulistButtonPart;
    } else {
      style.SetAppearance(kNoControlPart);
      return;
    }
  }

IsControlStyledという状態の場合でかつ、アピアランスがメニューリストでないものはkNoControlPartに切り替えるらしい。

IsControlStyled()の定義を見てみる。

bool LayoutTheme::IsControlStyled(const ComputedStyle& style) const {
  switch (style.Appearance()) {
    case kPushButtonPart:
    case kSquareButtonPart:
    case kButtonPart:
    case kProgressBarPart:
      return style.HasAuthorBackground() || style.HasAuthorBorder();

    case kMenulistPart:
    case kSearchFieldPart:
    case kTextAreaPart:
    case kTextFieldPart:
      return style.HasAuthorBackground() || style.HasAuthorBorder() ||
             style.BoxShadow();

    default:
      return false;
  }
}

<button>アピアランスkButtonPartなので、style.HasAuthorBackground() || style.HasAuthorBorder()を満たす場合、そのコントロールにはスタイルがついていると認識される。

HasAuthorBackground()の定義を見てみる。

bool StyleResolver::HasAuthorBackground(const StyleResolverState& state) {
  const CachedUAStyle* cached_ua_style = state.GetCachedUAStyle();
  if (!cached_ua_style)
    return false;

  FillLayer old_fill = cached_ua_style->background_layers;
  FillLayer new_fill = state.Style()->BackgroundLayers();
  // Exclude background-repeat from comparison by resetting it.
  old_fill.SetRepeatX(EFillRepeat::kNoRepeatFill);
  old_fill.SetRepeatY(EFillRepeat::kNoRepeatFill);
  new_fill.SetRepeatX(EFillRepeat::kNoRepeatFill);
  new_fill.SetRepeatY(EFillRepeat::kNoRepeatFill);

  return (old_fill != new_fill || cached_ua_style->background_color !=
                                      state.Style()->BackgroundColor());
}

だるくなってきたので深く追ってないけど、background-repeat以外のbackground関連プロパティが変わってたら指定されてるという判断なのかな。

続いてHasAuthorBorder()

bool StyleResolver::HasAuthorBorder(const StyleResolverState& state) {
  const CachedUAStyle* cached_ua_style = state.GetCachedUAStyle();
  return cached_ua_style &&
         (cached_ua_style->border_image != state.Style()->BorderImage() ||
          !cached_ua_style->BorderColorEquals(*state.Style()) ||
          !cached_ua_style->BorderWidthEquals(*state.Style()) ||
          !cached_ua_style->BorderRadiiEquals(*state.Style()) ||
          !cached_ua_style->BorderStyleEquals(*state.Style()));
}

border関連が変わってたらだめと。


なので、背景とborder関連プロパティ(border-image, border-radius含む)が変更されたら、そのコントロールにスタイルがあたってると認識され、マウスクリック時でもフォーカスリングが当たるようになる。

フォーカスリングを出したくない?

出したくない場合、borderとbackground関連のプロパティは使えない……ちょっと現実的でない。とはいえ:focus { outline: 0 }はダメゼッタイ。ジレンマ。

というわけで:focus-visibleなんて擬似クラスができた。Chromeで実装中なのでもうちょいでよさげな感じになる。

そういえば、Chrome以外でマウスクリック時にフォーカスリングを出すブラウザってあったっけ…?macOSだとFirefoxSafariは出さないみたいだけど、WindowsLinuxとかはどうだっただろうか。

Autoprefixerのブラウザ指定をpackage.jsonの`browserlist`からする

さっきAutoprefixerの設定でいろいろ阿呆なことをして、まあそれは解決して、ついでにリリースノートを見ていたら6.6.0でこんな機能が入っていたのを知る。

With Autoprefixer 6.6 you can specify browsers in package.json to reduce config files:

{
  "private": true,
  "devDependencies": {
    "autoprefixer": "^6.6.0",
    "postcss": "^5.2.6"
  },
  "browserslist": [
    "last 2 versions",
    "ie 9"
  ]
}

Note that we highly recommend specifying browsers in browserslist config or in package.json instead of Autoprefixer option.

これまでAutoprefixerのコードに書いてた対応させたいブラウザの範囲をpackage.jsonbrowserslistに書けるようになったとのこと。Browserslistcaniuseのデータをダンプしたやつを文字列でクエリできるやつで、いつからかAutoprefixerが使っていたのは知っていたけど、いろいろ使いやすくなってたんだね。不勉強であった。うあー

6.6.0時点でAutoprefixer以外にも、Stylelintやbabel-env-presetでも使われているとのこと。Autoprefixerは設定はコードじゃなくてpackage.jsonとかのがおすすめと書いてるので、今後はこう書いたほうがいいのかな。

書いてみる

手元のpackage.jsonとgulpfileをいじってみた。

  "private": true,
  ...
  "browserslist": [
    "last 1 version"
  ],
  ...

こんな感じで書いて、gulpfileでAutoprefixerをかましてるコード

  .pipe(postcss([
    autoprefixer({ browsers: ['last 1 version'], cascade: false }),
  ...

ここからbrowsers: ['last 1 version'],を削るだけ。
さて実行…うごいた…多分。エラーが出てもしかしてデフォルトにフォールバックされてるかもしれないけど、たぶん動いてる。わーい。

もうちょい読むと、package.jsonだけじゃなくて、$BROWSERSLISTっていう環境変数でもいいし、ルートに設定を書いたbrowserslistファイルでもいいらしい。READMEのQueriesなるセクションに優先順位とかが書いてあった。

Google Analyticsのデータを使う

これはBrowserlistというかcaniuseの機能なんだけど、caniuse.comのSettingsからGoogle Analyticsのアカウントを連携させてやると、グローバルなブラウザのシェアでなく、管理下のサイトのそれに連動した情報を出してくれる。

そのデータをなかなかなワンライナーでファイルとして出力してやると、そいつをもとにしてくれるとのこと。ファイル出力はもうすこしスマートにできるといいな。

するするさせたい:サイボウズ採用情報のアニメーション(その3)

サイボウズ採用情報のアニメーションがするするしてないので調べた件、今回は雑記。

  • その1 ― 何が起こっているのか調べる
  • その2 ― 調べながら直しながらするするさせる
  • その3 ― ちょっとしたことや他のブラウザでもするするさせる

描画領域とPaint Flashing

その1でPaint flashingを見たときにはアイコンの周りに枠がついていたのに、その2で描画領域を見たら文書全体で起こっていたたのをふしぎに思ったひとがいるかもしれない。

これはPaint flashingの説明 Hightlights areas of the page that need to be repainted というのをよく読むといいかも。緑色になったところは「再描画が必要になったところ」、paint invalidationを指している。なので変化していない箇所は緑色にならなかったわけだ。

Chromeにおける“Paint”が何を指すかは、Stack Overflowにある回答が詳しい。

モバイルはどうなのか

今回使ったのはふだん使ってるMacBook Air。2011年モデルだしAirだしMacとしてはだいぶしょぼい。それでもなんとかするするさせられた。

ではもっとしょぼい環境はどうなるか。というわけでNexus 5を繋いでリモートデバッグしてみた。 以下通常状態。MacBook Airと比べてCPUの使用率が高い。描画も引き続き高いけど、Scriptingも高いのが気になる。

f:id:myakura:20160515162406p:plain

transform: translateX() をつかったアニメーションに差し替えると、するするした。

f:id:myakura:20160515162413p:plain

いちおう大丈夫っぽい。ただ格安スマホなどもっとしょぼい端末だと、CPUやGPUがこれ以上に専有されるかもしれない。

iPhoneはどうだろう。iOSバイス持ってないので試せないのだけどiPhoneさまなら大丈夫だろうと勝手に思って眠ることにする(みんなは試そう)。

他のブラウザの開発者ツール

今回はChrome DevToolsを使った。アクティビティの詳細を見られるので説明に便利なのと、まあみんな使ってるんでしょということで。
ただChromeだけの最適化になってしまったらアレだ。他のブラウザはどうなのか。

FirefoxSafariのDeveloper Tools/Web InspectorにもふつうにElementsパネルはある。h で要素も隠せる。

Paint flashingもどちらにもある。FirefoxではハケのアイコンSafariはElementsパネルのパンくず右にあるブラシアイコンで同様のものが見られる。なおFirefoxは色相が常に変わり、Safariでは赤色のハイライトになる。

f:id:myakura:20160515163120p:plain

件のアニメーションはSafariでは真っ赤になった。赤いほうが問題っぽさが増していいかもしれない(Chromeも昔は赤色だったけれども)。

レイヤー表示。Safariの場合はPaint flashingアイコンの横にある四角が4つのアイコンを押すと表示される。

f:id:myakura:20160516063926p:plain

FirefoxではDeveloper Toolsからできない。about:config から layers.draw-borderstrue にすればいける。ちょっと面倒。Bug 877567なのかな。

で、肝心なタイムライン。Firefox, SafariどちらにもあるんだけれどChromeほど高機能ではない。Chromeが強すぎるというのはある。

ただ今回くらいのレベルであれば、そんな問題はないかも。数秒ほど録る、繰り返しを見つける、時間を見てみる。これでそれなりにわかるかなあと。描画領域についてはとりあえず will-change: transform をコンソールからぶっこんでレイヤー作って確かめるとかね。いろんなサイトのタイムラインを録ってみて調べるうちに感覚が養えてくると思うので、いろいろ見てみてほしい。

EdgeはWindows環境が手元にないのでわからないのだけど、ドキュメンテーションを見る限りは結構強力そう。

Safariでするする

さて、Safariでもとのページのタイムラインを録ってみた。

f:id:myakura:20160515172624p:plain

スタイル計算と描画、タイマー発火が頻繁そうなことがわかる。

f:id:myakura:20160515172649p:plain

Layout & Renderingというカラムを押したら詳細が見られる。Paintではその領域を見られるけど、これはinvalidationなのかな。ちょっとわからない。

Web Inspectorに新しく入ったRendering Framesモードでも見てみた。

f:id:myakura:20160515172657p:plain

1フレーム目にタイマー、2フレーム目に計算・描画が行われてて面白い。

さてするするさせてみる。transform を使ったコードをぶっこんでまたタイムラインを録ってみる。

f:id:myakura:20160515173631p:plain

するするした。同じ方法が使えるようだ。

なお、CSSアニメーション版のレイヤーを見てみると謎の水色の枠がでてきた。

f:id:myakura:20160516063943p:plain

こいつはなんなんだろう…Web Inspectorはドキュメンテーションがあまりなく、さらには更新されてないのでけっこうつらい。ソース読むしかないのか……

Firefoxでするする?

Firefoxでもみてみる。Firefoxのタイムライン(Performance)ツールは複数のタイムラインを保持できるのがうれしい。
現行のサイトを録ってみた。

f:id:myakura:20160516064037p:plain

あら、そのままでも60FPSに近い。Composite Layersにけっこう時間をとられているね。

f:id:myakura:20160516064046p:plain

Paintもすこしかかっている。描画範囲わからないのかな。もうちょっと調べないといけない。

f:id:myakura:20160516064106p:plain

CSSアニメーションに置き換えたものがこちら。タイマーはなくなったけど、ちょっとところどころフレームが落ちてしまっている。Composite Layersの負荷が変わってないので、もともと負荷が高いのかな。

f:id:myakura:20160516064127p:plain

あとスタイル再計算も走っている。Restyle HintがCSSアニメーションとなっているので、Geckoだとスタイルの再計算が走ってしまうのかな。ふむー

ただ見た目をいうと、CSSアニメーションのほうがするするはしているんだよね。データには現れづらいところなのかな…ちょっと心残り。


もうちょっとなんか考えてた気がするんだけど、メモしてなかったのともう書き疲れたのでこのへんで。
みんなもするするさせるんだよ。

するするさせたい:サイボウズ採用情報のアニメーション(その2)

サイボウズの採用情報ページにあるアニメーションがするするしていない。ので前回は何がおこってるのか見てみた。

今回はするするさせられないか、がんばってみる。

  • その1 ― 何が起こっているのか調べる
  • その2 ― 調べながら直しながらするするさせる
  • その3 ― ちょっとしたことや他のブラウザでもするするさせる

アニメーションを止める

まず、いまのjQueryベースなアニメーションを止める。
コンソールに以下をぶっこめばアニメーションが止まって、背景も初期位置に戻る。

jQuery('.icon').stop().css('backgroundPosition', '')

チェーンもできるしjQueryべんりだね。

CSSアニメーションにしてみる

jQueryのアニメーションを別の方法に書き換えるわけだけど、今は2010年代も後半だ。使うならCSSアニメーションしかない。とくに右から左へ一方向っていう単純な動きならなおさらだ。

さっきのjQueryのコードは、こう変換できるかな。

@keyframes loop-icon {
  from { background-position: 0 }
  to { background-position: -1680px }
}
.icon {
  animation-name: loop-icon;
  animation-duration: 60480ms; /* 1680×36 */
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  /* animation: loop-icon 60480ms linear infinite; でもOK */
}

で、これを document.head.innerHTML += <style>...</style> とかでくるんでコンソールにぶっこむとアイコンが動き出す。おー、なんとなくするする動いてる感じ。

描画の詳細を見てみる

なんとなくじゃだめなので、またタイムラインを録ってみる。

f:id:myakura:20160513131612p:plain

およ…?

CPUの使用量が下がった感じはするけど、引き続きジャンクな感じ。
拡大すると、たしかに下がってはいる。

f:id:myakura:20160513131746p:plain

あと、Summaryから“Scripting”の項目が消えている。これはjQueryのアニメーションが setInterval() ベースだったんだけど、それがなくなったからだ。

さて、多少の成果が出たけれど、これではするするとは言えない。もっと調べよう。
拡大して、太い緑色のところをクリックすると、その描画についての詳細を見られる。

f:id:myakura:20160513132737p:plain

描画範囲(Dimensions)が「1362 × 6935」となっている。なんと、アニメーションはページの一部しかないのに、どうやらページ全体を描画しているらしい。だから負荷が高いまま。

will-change で別レイヤーに移す

ページの一部(高さ120px)を動かすためだけにページ全体(6935px)が毎回描画されるのはだいぶつらい。再描画の範囲を最小にしたい。そこで注目するのが、さっきのSummaryにあった「Layer Root」という項目だ。

ブラウザの描画領域は一枚なときもあるけど、合成レイヤー(compositing layer)という複数のレイヤーからなっていることもある。今回のように一部だけ動くとか、そういう際にブラウザが自動的にレイヤーを切って管理してくれる。OHPシート重ねて動かすみたいなやつだ。

というわけで、ブラウザに「動くのここだけだからレイヤー切ってよー」というのを示して全体を描画させないようにする。これには will-change なるプロパティを使う。値にプロパティ名を指定すると、ブラウザはその要素上のそのプロパティが変化するのに適した処理をしてくれるようになる。

レイヤーを新たに作りたい場合、値に transform を指定するのがおまじないだ。

.icon {
  animation-name: loop-icon;
  animation-duration: 60480ms;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  will-change: transform;
}

古の -webkit-transform: translate3d(0, 0, 0) と変わらないじゃんと思うかもしれない。ハック感は否めないけど、でもいちおうオプトインだから。名前も「will-change」であくまで意図だしブラウザが今後もこの挙動とは限らないから(もごもご)……

レイヤーを確認する

レイヤーを切ったのはいいけれど、どう確認すればいいか。もいちどDevToolsのレンダリング設定メニューを開く。そこにある「Show layer borders」をチェックすれば、レイヤーの境界にボーダーが引かれるので、どこにレイヤーがあるかがわかる。

以下はもともとのページのレイヤー。レイヤーの境界は薄いオレンジで、ページの端についている。ページ全体が一枚のレイヤーみたいだ。

f:id:myakura:20160513140733p:plain

水色の水平線が見える。これはタイルというレイヤーのいち領域の区切りを示している。
右と下に緑色の矩形が見えるけど、どうやらスクロールバーの領域もレイヤーの一種みたいだ。

以下は .iconwill-change: transform を与えてレイヤー化したもの。アイコン周りに新しいオレンジの枠ができたのがわかる。

f:id:myakura:20160513140745p:plain

レイヤー分けた効果を確認

ではタイムラインを録ってみよう。

f:id:myakura:20160513142111p:plain

ふむ。ジャンクはまだあるけど、描画の負荷がだいぶ減っている。1フレームとばしで60FPSも達成している。またちょっと進んだね。

f:id:myakura:20160513142126p:plain

あんなに太かったPaintもほっそくなって、描画領域も「1362 × 120」に、レイヤールートも狙い通り #document から div.icon になった。

しかしまだジャンクがあるのが気になる。確かに見るとまだガタガタしている。
あと、Paintingが減ってはじめて気になったのが紫色のRendering。毎回なにをレンダリングしているのか?そう、背景画像だ。

transform でするする

実は、CSSアニメーションにしたから負荷が減るというのは限定的な話で、プロパティによってはレイアウト、スタイルの計算、描画がはしってしまう。レイアウトはスタイル計算を、スタイル計算は描画を引き起こすので、できるかぎり前の段階からの最小化が望ましい。

プロパティごとに引き起こされるレイアウト・スタイル計算・描画は、Paul Lewisらがまとめている。

最近Blink以外のエンジンも頑張っている。
さて、background-position の項をみると、it will cause painting to occur とある。なので描画は結局おこってしまう。あとスタイルの計算もはしってしまう。なのでjQueryまわりのスクリプト・レイアウトの負荷がなくなるだけで、コストの掛かる描画は残ったままになる。

どうすればいいか。アニメーションによるのでどうにもできないケースもあるだろうけど、今回のアニメーションであれば transformtranslateX() に置き換えるといい。transform は描画も起こさないので、この負荷も下げられる。というわけでこちらにしよう。

背景画像の繰り返しを利用してずっと流れているように見せているので、書き換えはちょっと工夫が必要。今回は .icon の幅を広げて、それを translateX() で動かすようにした。

@keyframes loop-icon-transform {
  from { transform: translateX(0px) }
  to { transform: translateX(-1680px) }
}
.kintone {
  overflow: hidden;
}
.icon {
  width: 3360px !important;
  animation: loop-icon-transform 60480ms linear infinite;
}

ウインドウの幅が大きいと端が見えちゃうだろうけれど、だいたいカバーできるだろう。
もうちょっといいつくりがあるのかもしれない。translateX() で動かせて、切れ端が見えないのならなんでもいい。

なお、今回は will-change を指定していない。というのも、指定しなくてもレイヤーを分けてくれたから。will-change もこのアニメーションの自動レイヤーわけもブラウザ依存なので、対応環境によってはつけないといけないかもしれない。

ではタイムラインを録ってみよう。

f:id:myakura:20160513143936p:plain

ててーん。ジャンクなし。だいたい60FPS。RenderingとPaintingも消えた。
見た目もするするしている。するするさせられた!


というわけで、するするさせたいという目標はいちおう達成できたかな。どうぞご利用ください。

ただちょっとだけ気になることとか、あと少し書いときたいこともある。そいつはまた次回。

(追記):書いたよー

おまけ:試してみよう

どんなコードをコンソールにぶっこんだかちょっと書いとこう。おためしあれ。

background-position 版のCSSアニメーションに置き換え。

jQuery('.icon').stop().css('backgroundPosition', '')
document.head.innerHTML += `<style>@keyframes loop-icon { from { background-position: 0 } to { background-position: -1680px } } .icon { animation-name: loop-icon; animation-duration: 60480ms; animation-timing-function: linear; animation-iteration-count: infinite; }</style>`

will-change: transform はStyleパネルから追加している。クリックでオンオフさせやすいので。

以下は translateX 版のCSSアニメーション(最終版)に置き換えるもの。

jQuery('.icon').stop().css('backgroundPosition', '')
document.head.innerHTML += `<style>@keyframes loop-icon-transform { from { transform: translateX(0px) } to { transform: translateX(-1680px) } } .kintone { overflow: hidden; } .icon { width: 3360px !important; animation: loop-icon-transform 60480ms linear infinite; }</style>`

いま考えたらTemplate Strings使ってるんだからべつに改行取っ払うこともなかったな…

するするさせたい:サイボウズ採用情報のアニメーション(その1)

サイボウズの採用情報ページを見ていた。

といっても受けるとかではなく、ただ性格悪いことを思っていただけなんだけど。

Kintoneのセクションでアプリっぽいアイコンたちが右から左へと流れているんだけど、それがガタガタとしている。するするしていない。するするさせたい。

というわけで、何が起こっているのかを調べてみようかと。何回かにわけて書くよ。

  • その1 ― 何が起こっているのか調べる
  • その2 ― 調べながら直しながらするするさせる
  • その3 ― ちょっとしたことや他のブラウザでもするするさせる

アニメーションの実装を調べる

あとのことを考えて、Chrome DevToolsを使う。

まず、該当の流れるアイコンのところで右クリックしてinspectする。Elementsパネルで、<div class="icon"> という要素がハイライトされる。div.icon には style 属性がついていて、そこにセットされた background-position の値が絶えず変化しているのが見える。

f:id:myakura:20160510083150p:plain

というわけで、背景画像の位置をJavaScriptで逐次書き換えるっていう、クラシックなアニメーションの実装方法なのがわかる。

使われているJavaScriptファイルのソースを見てみる。.icon で検索すると見つかった。

var loopIcon = function(){
  $(this).css({backgroundPosition:'0'});
  $('.kintone .icon').animate({backgroundPosition:'-1680px'}, 1680*36, 'linear', loopIcon);
}

jQueryでやっているみたい。で、こういうJavaScriptで逐次CSSを書き換えていくアニメーションにはたいてい描画絡みの問題がある。

描画されている箇所をみてみる

描画について見てみる。DevToolsのレンダリング設定パネルを使う。

DevToolsのメニューで「Other tools」→「Rendering settings」を選ぶと、下からにゅっとConsoleなどがついたパネルがが生えてくる。Renderingタブが選択されていると思うので、その中にある「Enable paint flashing」をチェックすると描画の変化する箇所に緑色の枠がつく。スクロールしたり、リンクにカーソルを重ねたりすると、「描画の変化」がどういうものかわかると思う。

f:id:myakura:20160510083928p:plain

さて件のアニメーション。ずっと枠がついたまま。なので、この箇所が絶えず描画されているのがわかる。

タイムラインをとってみる

こいつがパフォーマンスにどれくらい影響しているのか、もうちょっと見てみる。
Timelineパネルを開いて、左上にある黒丸を押して2〜3秒ほど録る。

以下はMacBook Air 11" (mid-2011モデル、メモリ4GB)上のChrome Canary (M52)、ちょこちょこブラウザやエディタを開いた状態で録ったもの。記録する際に、PaintとScreenshotのみチェックした。

f:id:myakura:20160512120322p:plain

とれたけどいろいろ情報が多いので、まず上のオーバービューと呼ばれる箇所を見る。その下のフレームチャートなどはあとから見よう。

オーバービューは、フレームとFPS、CPUのアクティビティ、スクリーンショットを見せて大まかなところを把握するためのところ。

f:id:myakura:20160512120342p:plain

フレームのところにあるがたがたした緑色の領域はFPSで、てっぺんが60FPSを指す。その上に見られる赤い線は60FPSを切ったフレームで、いわゆる「フレーム落ち」、海外のWebパフォーマンス界隈だと “jank” と呼ばれるやつを指している。
つまり上に赤い線が見えず、緑色の面積が大きいほどいい。

で、かなりのところでジャンクが発生している。環境によっては赤いのを見ないかもしれないけど、今回はこうなった。

さて、フレームの下、CPUのアクティビティを見る。ちょこちょこと濃い緑色の山があって、たまに紫や橙が見られる。
上のフレームのとこと違って、こちらは山が低い方がいい(負荷が少ないから)。ではこの山になってるとこで、なにがおこっているのか。

フレームチャートを見る

調べるといってもどこを見るといいか。見直すと、ちょっとパターンが感じられる箇所があった。

f:id:myakura:20160512124009p:plain

だいたい等間隔で高い山が現れているので、ここを詳しく見ると、パフォーマンスの問題が見えてきそうだ。そこを見てみよう。
オーバービューのところで範囲選択すると、下のフレームチャートが該当する範囲のものに切り替わる。

f:id:myakura:20160512131823p:plain:w600

一番下のペーンに、アクティビティごとにかかった時間が円グラフにまとめられている。“Painting”と“Other”が多い。後者はさておいて、描画関連の処理は全体400ms中38msと、1割弱もの時間がかかっているのがわかる。

本当にそこなのか、隠してみる

さて、タイムラインを録ってアイコン周りの描画の負荷が高くてフレームが落ちているようになんとなく感じていたものの、それが直接の原因なのかを確かめていなかった。どうすればいいか。隠せばいい。

もういちどElementsパネルに戻って、div.icon を選択し、h キーを押す。そうするとアイコンがぱっと消えてしまう。

f:id:myakura:20160512141442p:plain

div.icon__web-inspector-hide-shortcut__ なんて変なclassがついたけど、中身は visibility: hidden を指定しているだけ。
visibility: hidden が当たると描画関連の処理が行われなくなる。ではこの状態でタイムラインを録ってみよう。

f:id:myakura:20160512142003p:plain

全体的にいろいろ薄くなった。ジャンクな赤色も見えない。FPSのグラフがなくなっているのはアイドル状態になった、つまりは活動しなくてもよくなったから。


というわけで、アイコンのアニメーションが描画に負担をかけてるのがわかった。では負担をかけない、するするするアニメーションにどう作り変えていけばいいだろうか。長くなったのでそいつは次回に。

(追記):次回書いたよー

するするさせたい

昨年くらいからWebの描画まわりのパフォーマンスについて調べたりしている。
何をしたいのかと言うと、するするさせたい。

するする?

あくまで自分の傾向なんだけど、読み込まれるまで時間がかかるというよりは、ページのスクロールでひっかかりがあったり、クリックしたときのアクションがブロックされたりする方が、不満がある。

そこらへんをなんとかしたいなと。

もうちょい具体的に

もうちょっと技術的に言うなら、余計なreflowやrepaintをなくすとか、render blockingな要因を減らすとか、そういうところだろうか。後者については、するする関係ないと言われるかもしれないけれど、そこも描画に依存するところなので……

というわけで

そこら辺のこと書いたりお仕事にしてこうかと。仕事になるかなー……

Progressive Web Appsとは

Chrome Dev Summitに来ている。

今年のChrome Dev Summitは日ごとにテーマが分かれていて、初日がProgressive Web Apps、2日目がRAILらしい。RAILはGooglerがちまちまと話してるけど、前者についてはまだそんな離されてない気がする。

というわけでセッション聞きながらなんとなく書いてみよう(なのでセッションまとめではないよ)。

アプリに「なっていく」「Web」

Progressive Web Appsは、Alex Russell先生が6月にブログで提唱して、そこからプロモートしているもの。“progressive”を訳すと「漸進的」やら「進行性」とかになるけれど、そのまま訳すより「アプリになっていくWeb」と砕いて解釈するとわかりやすそう。

アプリに「なっていく」とは何かなんだけど、Webアプリが「使ってるうちにネイティブアプリと同じような体験を備える」ってところなのかな。なので基本的にWebアプリというのは変わらない。

また、「Web」というのもポイント。「『Web技術』を使ったアプリ」ではなくて、サーバにホストされURLを持つ、「Web上にあるアプリ」というのが重要。

なんとなく「モバイルをキャッチアップする」というのに向いていたここ最近のWebプラットフォーム。ブラウザのパフォーマンス改善やら、新しいAPIなどよい点もあったけれど、ちょっとそこが行き過ぎてしまった感もある。モバイルであんま使えそうにないAPIいらなくねって言って顰蹙買ったりとかね。

Progressive Web Appsはそこからちょっと立ち返って、「インストール不要で使える」「リンクできる」「ストアなどの制限なく誰でも自由に開発できる」といったWebの性質を活かすことに主眼を置き直し、そのうえで発展を見ているように感じる。

でも、いい点だけ押してても特に何も変わらない。PWAはモバイルのアプリと比べて厳しかった「エンゲージメント」に着目して、そこを改善して、そして辿り着けたものかなあと。

アプリになっていくには

「アプリに『なっていく』」っていうのはどういうことなのか。Webサイトがどうなったらアプリとして感じられるようになるのか。

ひとつは、アクセスの起点。Webアプリの場合、ブラウザのアドレスバーから入力したり検索したりでたどり着くことが多いだろう(WebページならSNSや検索からのが多そうだけれど)。一方でアプリはホーム画面やローンチャにあるアイコンから立ち上げることがかなり多いと思う。どっちが「アプリ」として楽かというとたぶん後者になる。じゃあ、Webアプリをホーム画面とかに登録できるようにすればいい。ホーム画面にはアイコンがついて、タップしたらアプリっぽくWebアプリがブラウザで開かれるといい。

ひとつは、起動した時の感覚。「アプリ」として使うには、オフラインでも「ガワ」や最低限のコンテンツは表示させておきたいし、単体で完結できるものならロケーションバーとかは消しときたい。そういう風にさせられればいい。

ひとつは、起動してない時の関係。ブラウザを開いてない時でも、提示されたい情報や、アップデートされてると嬉しいデータなんかがある。そうさせよう。プッシュ通知とか同期ができるようになるといい。

そしてこういう体験は強制ではなく、それを好みそうと思う人に、適当なタイミングで促せばいい。ブラウザがアクセスしたパターンをみて、アプリとして使ってそうだったら「もっとアプリっぽくしてみない?」と言う。Webアプリが「アプリインストールしない?」とか、最近見ないけど「ホーム画面に登録してみない?」っていうのを勝手実装するんじゃなくて。

あたらしい?

具体的な技術を挙げるなら、Web App Manifest、Service Workerがインストールまわりを、Push APIとNotifications API、Web App Manifestの一部メンバがエンゲージメントまわりを担当する。

ただ、こういう新しい機能を待たずとも、やろうと思えばそれなりにできていただろう。Service Workerで初めて広くもたらされるのはプッシュくらいで、オフラインなどはAppCache使えば(つらいだろうけど)なんとかなる。ホーム画面への追加も、ブラウザの機能で前からある。

マーケティング用の単語というとそうかもね、というところ。でも概念に名前つけるのは、広めることを考えるとそれなりに重要だろう。ふんわりした「Webアプリ」の再定義やオーバーロードはしづらいだろう。あとは開発はそれなりにつらいものだっただろうから、PWAを前提に機能スタックを整備し、その上で名前をつけられたので、そんなに無意味ではないかなあと。

すこし開かれたWHATWG HTML

8月末のことなんだけれど、WHATWG HTMLの仕様書GitHubに移った。

体制も少し変わって、HixieがひとりでやっていたHTML仕様の作業にAnneやDomenicなども直接関わるようになった。Issuesでの議論やPull Requestの受け付けも始まり、だいぶモダンな策定環境になった。

これまでのWHATWG HTMLとHTML5仕様書

これまでのWHATWG HTMLは長らくSubversionサーバで運用されていた(もしかするとCVS時代もあったかも)。

HTML仕様書は基本的に1枚のHTMLファイルに書かれていて、それをツールで切り出しmultipage版やPDF版、Web Developer Editionなどを生成していた(あ、<picture>については、Simon PietersがRICGで作業してたものをパッチとして取り込んでいたので、多少いびつなことになっていたけど)。

W3CでのHTML5(関連)仕様書も、W3C版のパッチが必要なところや、Canvas 2D仕様とかの分割にはソース中に特殊なコメントを入れてツールがそれをもとにそれぞれの仕様書をつくるなんてことをしていた。

W3C仕様書は統一的なツールが最近になってようやく運用されはじめたけど、それ以前のものは仕様やEditorによって違っていて、それが他のEditorの参入障壁になっていた気がする。

GitHubでのコミュニケーション

仕様(書)がオープンな環境にうつったこと、またHixie以外の人が積極的に関わり始め、多少なりとも変化が感じられる。

ひとつはW3C HTML5仕様との差を埋める提案が少しずつ出たこと。たとえばパーザは<rb>などに対応した(conformingになってないのは厄介だけど)し、ARIAとの関わりについてはSteve Faulknerの仕様を参照するように変更された。

とはいってもこれはHixieが拘りをなくしたところなだけで、そうでないものはやっぱり手強そう。<main>の違い(複数書けるか書けないか)についてはIssueがたったもの燃えている。<hgroup>消そうみたいなIssueが経つと言うとまた荒れるんじゃないかな。Hixieの考えが悪いってわけでは必ずしもないんだけど、GitHubで運営されているものが持つオープンさとはだいぶ差があるのでそこらへんどうなるかな。バランスとれるといいんだけど。

HTMLはどうなってくのかね

HTMLの策定は、集まったユースケースやデータを元にHixieがうんうんと考えデザインされたものが仕様に出てきて、それをレビューするというフローだった。Web技術の策定にかけるリソースが足りてない2000年台前半から半ばは、執念あるひと(Hixie)が居ることで仕様やWebが進んだ。

ただ、デザインされた機能が実際に使えるものになるとは限らない。自身でも失敗と認める機能(AppCacheやWeb Storage、History APIなど)は少なくない。欲しいものではなかったことがわかった時にはもう遅いなんてことがあった。

Hixieはdeclarativeな機能を好んでるふしがあって、それはたぶんUAの独自性やオープンな競争を尊重してたからだと思うのだけど、同じ挙動というかなり強い互換性が求められる昨今にはあまり好かれるものではない。そういう反省もあってExtensible Webというのが来ていたりもするので、彼の設計思想がふんだんに反映されてるであろうHTMLをどれだけ綺麗に崩していけるかが、HTMLを発展させるポイントなのかなと。とはいえ彼を説き伏せるのはだいぶ至難の業だからねえ。

ただ、HTML自体にどれくらい発展が求められているのかはちょっとわからない。新しい要素ならWeb Componentsで自分でやってねということになる(そうさせたい)だろうし、その他APIはメンテしたりたまにちょっと足したり、他の仕様でさせるためのextension pointを作ったりするくらいで、基本的にはメンテナンスモードのままなのかもしれない。バージョンレスとは言ったけれど、バージョンを切るほどの活力はもうないのかなとも思う。

Content ScriptsなChrome拡張をFirefox拡張に

追記(2015-10-16):WebExtensionsの発表

これを書いた数ヶ月後の8月21日、Mozillaが今後のアドオンをWebExtensionsというChrome拡張APIベースなものに移行していく計画を発表した。

なので、今後のことを考えたりもともとの「Chrome拡張をFirefoxでも動かしたい」というのを考えるとWebExtensionsベースなものにしたほうが良さそう。実際に試したところ、Chrome拡張のmanifest.jsonにWebExtensionsで必要なメンバを足しただけでふつうに動いたので、Content ScriptsベースなChrome拡張ならほとんど手間を書けずにクロスブラウザな拡張にできそう。

というわけで、このエントリで書いたAdd-on SDKベースな拡張はいささかレガシーなものになりそうだけど、気になる方はお読みくださいな。


お手製のしょうもないChrome拡張をFirefox拡張にしてみた。といってもタイトルにある通りContent Scripts、つまりユーザースクリプトレベルなものなので、そんな面倒ではなかった。

Firefox拡張のContent Scripts

まずは同じコンセプトのものがFirefoxにあるのか、ドキュメントを探す。あった。

Many add-ons need to access and modify the content of web pages. But the main add-on code doesn't get direct access to web content. Instead, SDK add-ons need to factor the code that gets access to web content into separate scripts that are called content scripts. This page describes how to develop and implement content scripts.

Content scripts can be one of the more confusing aspects of working with the SDK, but you're very likely to have to use them. There are five basic principles:

  • the add-on's main code, including "main.js" and other modules in "lib", can use the SDK high-level and low-level APIs, but can't access web content directly
  • content scripts can't use the SDK's APIs (no access to globals exports, require) but can access web content
  • SDK APIs that use content scripts, like page-mod and tabs, provide functions that enable the add-on's main code to load content scripts into web pages
  • content scripts can be loaded in as strings, but are more often stored as separate files under the add-on's "data" directory
  • a message-passing API allows the main code and content scripts to communicate with each other

拡張からページのDOMは直接触れられずContent Scripts経由というのはChromeと同じらしい。あとIsolated Worldというコンセプトも共通。

スクリプトの読み込み方は違いがあるけど、Content Scriptsのファイルはそのまま流用できそう(Chromeでしか対応してないものを使ってない限りは)。わーい。

jpm

Firefoxの場合はどうやらAdd-on SDKをインストールしないといけないらしい。これまではそのSDKPythonベースのやつだったらしいんだけど、Fx38からNodeベースのjpmというものになったとのこと。

というわけで npm install -g jpm でさくっとインストール後、jpm init で名前やらエンドポイントやらを設定。

Content Scriptsの読み込み

Chrome拡張では manifest.json からContent Scriptを動作させたいURL(のパターン)とそのファイルを明示的に指定していた。

...
"content_scripts": [
  {
    "matches": [ "https://foo.bar/*" ],
    "js": [ "baz.js" ],
    "css": [ "quux.css" ]
  },
  ...

Firefox拡張の場合はエンドポイントのファイルを用意して、そこからプログラマブルな感じで指定するらしい。jpm initした場合はindex.jsができる。

今回の拡張はページにスクリプトファイルをぶっこむものなので、page-modというAPIを使う。

const data = require('sdk/self').data
const pageMod = require('sdk/page-mod')

pageMod.PageMod({
  include: "https://foo.bar/*",
  contentScriptFile: [ data.url('baz.js') ],
  contentStyleFile: [ data.url('quux.css') ]
})

ファイルの置き場

エンドポイントを指定したら jpm init したディレクトリ直下にできたのだけれど、Content Scriptsのファイルとかはその中に data ディレクトリを作ってそこに置かないといけないらしい。ここで少しはまった。initしたら作ってほしいな。

Chrome拡張はとくにディレクトリわけとかしてなかったので、そのまま読み込めないか試すもアウト。どうやらファイルを指定したとこで使ったself.data.url()data ディレクトリ内のファイルのみしか読めないらしい。まじか…

気になったのでソースを見てみる。

exports.data = Object.freeze({
  url: uri,
  load: function read(path) {
    return readURISync(uri(path));
  }
});

はいはい。では uri はというと…

const uri = (path="") =>
  path.contains(":") ? path : addonDataURI + path.replace(/^\.\//, "");

ええ。それで addonDataURI はというと、

const addonDataURI = baseURI + "data/";

(´Д`)

固定だ…ちょうmagicだ……

というわけであきらめかけてたんだけど、ふと data.url('../hoge.js') としてみたら読み込めた。でもいいのかな…まあ動かなくなったら考えよう。

ディレクトリ固定、もやっとすることはあるけど拡張のレビューとか、他のひとの拡張を知りたいときには見るところが決まっていて便利なのだろうなとも思う。Chromeだとmanifest.json見ないといけないし。

その他

作った拡張は jpm xpi でパッケージ化すれば再起動不要なアドオンとしてインストールできる。

Content Scriptレベルならふつーにできたけど、UIになにかぶっこむとかそういうのはAPIがだいぶ違うので、できるだけモジュールに落とすとかしないと共通部分が減るばかりな気がした。試したいんだけどそういう拡張を作ったことがない(…)ので、なんかアイデアを持とう。

FirefoxのCSS Unprefixing Service

追記(2016-09-23)-webkit-接頭辞のサポートを一部のサイトのみ対象とする方針はその後方針転換し、すべてのサイトを対象とすべく必要な機能やエイリアスの実装が行われ、Firefox 49でリリースされた。

標準での対応については、もととなる機能を標準化したうえでCompatibility Standardエイリアスを定義している。ただwebkitMatchesSelector()CSS Animationsのイベントなどは、DOM仕様に組み込まれていたりも。

というわけで、ここで取り上げたlayout.css.unprefixing-service.enabledは意味をなさなくなる。接頭辞のエイリアスなしでも動くかどうかは、layout.css.prefixes.webkitという設定がCSSについては用意されている。


先日about:configにlayout.css.unprefixing-service.enabledなんてのを見つけて「なんだろう」と思ってたのだけど、Compatibility Teamのメーリングリストにアナウンスがあった。どうやら-webkit-CSSプロパティとかがあったら、それを標準のコードとして解釈するものらしい。

This feature is only active for the "top 10" broken Chinese mobile sites that Peipei provided[1] (e.g. several Baidu services and m.taobao.com). The whitelisted sites should have a much-improved user experience, due to Firefox converting legacy -webkit prefixed CSS into equivalent unprefixed CSS.

Mozillaは2012年にもWebKit接頭辞について検討していて、当時はCSSだけでは意味がないと判断していた。ただFirefox OS端末の投入とかで状況が多少変わったんだろうか。それともUA SniffingAPIについても似たようなことやってるのかな?

まだサービスの実装も途中らしく、GradientsやCSS Animationsはサポートされてないとのこと。CSS AnimationsについてはYahoo! JAPANもWebKitのみらしく、このままだと登録しないとかもねなんて話が出ている。ヤフーさん!!!Yトップなんとか!!

PrestoやProject Spartanではすべてのサイトを対象に接頭辞の処理をしている(みたいだ)けど、Mozillaの場合は特定サイトのみに限定させるみたい。特定サイトだけの対応はIEのCompatibility Listの膨れっぷりを見ると茨の道な気もするけれど、がんばってほしい。

SassScriptでリスト関数を拡張する

その昔Sassに凝っていたころに書いたメモを見つけた。供養のために公開。

当時のSassにはセパレータ判別の関数とかなかったので、今は特に必要ないものもあるね。時間経ったね。


グラデーションのmixinを書いたときに「リストを扱う関数が足りないなあ」と思ったので、関数をいろいろ書いてみている、RubyじゃなくSassで。

is-list($arg)

リストかどうかを返す関数。list 型のほか、可変長引数の arglist 型でも true を返す。

@function is-list($arg) {
  @return type-of($arg) == 'list' or type-of($arg) == 'arglist';
}

ただこれ、ちょっと問題があって、('item') などひとつの値を括弧でくるんだものを渡すと false を返してしまう。

$list: ('item');
@debug is-list($list); // DEBUG: false

上の場合 $liststring 型になって結果 false になる。なんでかというと、Sassでは括弧がリストのデリミタではないので、括弧で包んでもリストにならないから。えー……

ただ、もし括弧をデリミタにすると、演算の優先順位を変える括弧と区別がつけられず、(3 + 4) * 5 という式が「3 + 4 → 7 という値の入ったリストに5を掛けている」など解釈されてしまう。ううむ……

ただ、それはそれで困ったもので nth()index() にリストを突っ込んだつもりがアイテムがひとつだったためエラーになったりした。幸い、この前リリースされたSass 3.2.7でリスト関数についてはアイテムがひとつでもリストとして解釈されるように修正されたのでよかったんだけれど、type-of() の挙動は引き続き変わっていない。まあ、仕様なのでちょっと諦める。

余談:アイテムひとつのリストを作る方法

アイテムひとつのリストを作れないわけではない。空のリストをつくって、そこにアイテムをいっこ加えればよい。こうやって作ったリストは型がちゃんと list になる。

$oneitemlist: append((), 'item');
@debug type-of($oneitemlist); // DEBUG: "list"

contains($list, $value)

ある値がリスト内にあるかどうかを返す関数。

$fruits: 'apple' 'banana' 'cherry';
@debug contains($fruits, 'banana'); // DEBUG: true

実装は index() を使っている。

@function contains($list, $value) {
  @if not is-list($list) {
    @warn 'argument error: #{$list}';
    @return 'error';
  }
  @return type-of(index($list, $value)) == 'number';
}

index() だけで済みそうなものだけど、Sassの index() は値が見つかない場合に false を返すので、JavaScriptでやるような indexOf(item) > -1 みたいなのができない。なので type-of() を使い index() が数値を返すかで判定している。

has($value, $in)

has()はただ、contains()引数の順序が替わっただけ。内部的には contains() を使ってる。

@function has($value, $in) {
  @return contains($in, $value);
}
$oyatsu: 'umaibou' 'miyako-konbu' 'big-katsu';
@debug has('banana', $oyatsu); // DEBUG: false

Sass組み込みのリスト関数は第一引数にリストを取るような設計なんだけど、mixinとかfunctionを書いてると、リストではなく値を前に出したほうがコードが読みやすくなるなと思ったことが結構あったので書いてみた。

あと、has()は引数のキーワードを変えている。これでどうなるかというと、ちょっとヒューマンリーダブル感が出る感じに書ける。

$oyatsu: 'umaibou' 'miyako-konbu' 'big-katsu';
@if has('banana', $in: $oyatsu) {
  content: 'わーい';
}

む、書いてみたけど、そんな分り易くないというか、読みにくくなってるだけかも……

separator($list)

Sassのリストはカンマ区切り、空白区切りと、ふたつの書き方がある。そのどっちを使っているか検出する関数。

@function separator($list) {
  @if not is-list($list) {
    @warn 'argument error: #{$list}';
    @return 'error';
  }
  @if length($list) == 1 {
    @return null;
  }
  $list_comma: ();
  $list_space: ();
  @each $item in $list {
    $list_comma: append($list_comma, $item, comma);
    $list_space: append($list_space, $item, space);
  }
  @if $list == $list_comma {
    @return 'comma';
  }
  @else if $list == $list_space {
    @return 'space';
  }
  @else {
    @return 'error';
  }
}

アイテムとその並びが同じでも、セパレータが違うと同一とみなされないことを利用している。

@debug (1, 2, 3) == (1 2 3); // DEBUG: false

中身は同じでセパレータが違う2つのリストを append() しまくってつくってるので、実行コストが多分高い。

reverse($list, $separator)

項目が逆に並んだリストを作る。オプションでセパレータを変更可能。

@function reverse($list, $separator: null) {
  @if not is-list($list) {
    @return $list;
  }
  @if not has($separator, (space comma)) {
    $separator_orig: separator($list);
    @if has($separator_orig, (space comma)) {
      $separator: $separator_orig;
    }
    @else {
      $separator: space;
    }
  }
  $i: length($list);
  $result: ();
  @while $i > 0 {
    $result: append($result, nth($list, $i), $separator);
    $i: $i - 1;
  }
  @return $result;
}

セパレータを変更可能にする必要なんて(たぶん)ないのだけど、もとのセパレータ知らないと返すリストのセパレータをどうすればいいか悩む→セパレータ検出しよう→じゃあ変更オプションもつけようか的な。こうして無駄な機能が増えていくのだろう……

is() ― なんでも系

引数のキーワードでヒューマンリーダブルにしよう計画をやっていてひらめいた。こんなのはどうだろう。

@function is($args_list) {

  $value: nth($args_list, 1);
  $keyword: nth($args_list, 2);
  $target: nth($args_list, 3);

  @if $keyword == 'in' {
    @return contains($target, $value);
  }
}

こう使える。

$fruits: 'apple' 'banana' 'cherry';
@debug is('banana' in $fruits); // DEBUG: true

関数の引数を「スペース区切りのリストひとつ」として扱って、引数を自然文ちっくに書けるようにしている。引数の数は固定しといて、キーワードに応じて処理を振り分ける。キーワードを増やせば拡張可能。

@function is($args_list) {
  ...
  @if $keyword == 'in' {
    @return contains($target, $value);
  }
  @if $keyword == 'typeof' {
    @return type-of($value) == $target;
  }
  @if $keyword == 'not' {
    @return $value != $target;
  }

is(A not B) が要るかどうかはさておき、こんな感じに拡張可能。


という書きかけでした。ほんとはもうちょっと考えてたはずなんだけど忘れてしまった。

RubyではなくSassでというの、当時はアレ感しかない(まあ今もそうか)けど、libsassの隆盛でRubyでの拡張がちょっとという感じもあるし、本体側の拡張よりもSassScriptでできたほうが今後はいいのかもなと少し思った。速く動くのかどうかは知らないけれど。