フロントエンド: 支えなくて良い技術を支える技術


この記事は、mediba Advent Calendar 2018 の記事です。17 日目は au パートナー本部 / au サービス開発部でフロントエンドエンジニアをしています、武田 @tkdn が担当します。

先日 フロントエンド全身ちぎれ節 支えなくて良い技術を支える技術 と題した LT をさせていただいたのですが、そちらの拡張版になります。


伝えたいこと

  • 支えなくて良い技術
  • 支えなくて良い技術を支える技術
    1. webpack + Babel + babel-preset-env
    2. shim, polyfill
    3. 固有バグとの付き合い方
  • まとめ

支えなくて良い技術とは

deprecated = 非推奨, obsoleted = 廃止 された技術のことをここでは指します。紙や車と違って、Web はたかだか 30 年くらいの若い産業なので、技術自体の隆盛や衰退のスピードが激しく、様々な策定プロセスの中で少し前の技術が非推奨・廃止となるスピードも他の産業とは桁外れに早いと思っています。

フロントエンドやブラウザ

自ら課題を作り出して解決していると自作自演のような誤解さえ生むことが多いフロントエンドですが、ことさらにスピード感が激しく外野から見ているとそのように見えるのではないかと思います。

我々の主戦場であるブラウザにおいては(リリースサイクルの早いブラウザにおいては)日々新しい API が生えていきます。と同時に、策定の中で提案が行われブラウザに先行実装されたものの、途中で非推奨・廃止もしくは提案自体の却下があった API がブラウザから消えることもままあります。

フロントエンドに関連する技術も然りで、OSS としてメンテをやめたもの、別のスタックへ移行するため現行バージョンは開発を続けないもの、セキュリティの観点から使用を止めざるを得ないもの、日々振り回され情報をキャッチアップしていかないと、ちょっと前の情報がほとんど陳腐化していることも多くあります。2018 年のトピックを任意で抽出しました。

TLS 1.0/1.1 のサポート停止

昨年ごろから各社のサービスでアナウンスされることが多かった、TLS 1.0/1.1 はどのブラウザも 2020 年〜無効化を行うことを発表していたりします。今年 6 月に Yahoo! Japan からアナウンスがあったのを覚えている方もいるのではないでしょうか。

これによってセキュリティ上の観点からサポート対象外とすべき OS・ブラウザが出てきます。

Android 2.x, 4.x などはほぼサポート対象外となるため、今後 Web ブラウズすることが出来なくなる端末として考えて良さそうです。

が。

弊社では大人の事情が加味され、まだ 2.x, 4.x でのブラウジングをサポートしています(もちろんサービスを提供する事業者としても適切なアナウンスも行っています)。

前置きが長くなりましたが、 これらの支えなくて良い技術(を備えている OS・ブラウザ)を支える技術についてすこしご紹介します。

支えなくて良い技術を支える技術

クロスブラウザ対応はもちろんフロントエンドの責務なわけですが、いわゆるレガシー端末への対応は全て疲弊します。

  • 端末や OS 由来によるバグチケット
  • かさむ工数
  • たまるカルマ
  • 開発体験 DX の悪化

特に、レガシー端末のお守りを続けるのは開発体験を著しく損ない、日々のコードや設計に必ず漏れ出てきます。ワークアラウンドな対応やハック、不要なフォールバックと忘れられていくフォールバック用のコード…などなど。集中すべきことに集中できない状況が続くと、開発者は疲弊していきます。それらを支援し、支えていくためには下記のようなことが必要そうです。

  • レガシー端末をフォローしていくことに疲れない
  • 開発者が(ある程度)意識せずコーディング・オペレーションできる
  • 思考しなくて良い部分は自動化し集中すべきところに集中する

このためにどういったアプローチで開発に取り組んでいるか、技術的なご紹介をします。

1. webpack + Babel + babel-preset-env

どれも昨今の JavaScript を取り囲むエコシステムの中で耳にされることも多いと思うのですがあらためて。

  • webpack = ES modules / CommonJS 等のモジュール解決を行うバンドラー
  • Babel = ES2015 以降の構文を ES5 相当の構文に書き下してくれるトランスパイラ
  • babel-preset-env = browserslist の提供する DSL を記述することでターゲットブラウザに合わせたトランスパイルが可能
{
  "browserslist": [
    // Android 2.1 以上に最適化
    "Android >= 2.1",
    // iOS 8 以上に最適化
    "iOS >= 8",
    // IE10 に最適化
    "ie 10"
  ]
}

開発者が OS やブラウザを意識することなく ES2015 以降のモダンな構文を使って JavaScript を書くことができます。レガシー疲れを防ぐために、開発者が意識することなく、トランスパイルが自動化を担ってくれるので、当初の目的は達成できたように思えます。

ただし、これだけでは埋めることができないものがあります。

2. shim, polyfill

お伝えしたように日々ブラウザには新しい API が足し引きされていく状況の中で、開発が止まったレガシーなブラウザには不足した API がいくつも存在します。ある程度は前段の babel-preset-env がトランスパイルの段で構文の静的解析を行い API を継ぎ足してくれますが、明示的に polyfill と呼ばれる API 単体を埋めるためのコードを読み込む必要があります。 モダンなブラウザが日々足し込む API だけならまだしも、Android 2, Android 4.0 のような ES5 相当の JavaScript ネイティブ API が存在しないレガシー端末もサポートしなくてはなりません。

ファイルサイズの肥大化が気になりますが、es5-shim をバンドルするファイルに含めて、自動化に組み込むことにしましょう。さてこれで大丈夫、と言いたいところですが、残念ながら一部の Android 2 系には、ES3 相当の記述をしないと対応できません。

つい最近、MS を偲び、ここに IE6 対応 SPA の作り方を記す という記事が話題になりましたが、ネタではなくこちらは割と本気でプロダクトコード化する必要があるのです。

// object のプロパティが予約語となる場合エラーとなるなど

// 下記の構文がエラーとなる
var obj = { class: true };
var obj.class = true;
// こうしないとだめ
var obj = { "class": true };
var obj["class"] = true;

es3ify な webpack plugin もありましたが webpack 4 と相性がよくなかったため、上記のパッケージを使って webpack がバンドルしたファイルを後処理でさらにトランスフォームするという方式を採択しました。

これで何とかほぼ網羅しきったと思います。

しかし、レガシーな端末は ECMAScript の規格とはまた別の問題を抱えています。OS, ブラウザ, また端末固有のバグです。

3. 固有バグとの付き合い方

レガシーな端末は基本的にもうアップデートが行われないため、WebKit の特定のバージョンからもはやアップデートされないブラウザ、Chromium の途中から派生させベンダーが独自で作ってしまったブラウザ、など状況がとても複雑です。これらをすべて網羅することは人智を越えており、開発者に 全てを掌握させることはまず無理です。とはいえ、これといってまとまった資料やリソースもオンラインには存在していないと思っています(あったら教えてください)。

その中でも JavaScript V8 エンジンのバグが解消されずそのままの状況であるということも起こりえます。

あるとき、Android 4.x のみエラーが出るという報告を受けました。コンソールが見ることができたので、ブラウザを見るとエラーが出ています。

Uncaught TypeError: Cannot assign to read only property '__esModule' of #<Object>

__esModule とあることから、webpack 側のなにかであることは突き止めましたが、すぐにはわかりませんでした。よくよく調べてみると、これはどうやら Android 4.0.x に搭載された JavaScript エンジン V8 のバグ起因であることがわかりました。

V8 のバージョンまで特定は出来ませんが、Android 4.0.3, 4.0.4 の標準ブラウザもしくは webview の JavaScript V8 エンジンではバグを持ったまま実装されているようです。なお 2.x 系ではエンバグしないということも気付けなかった点です(動作保証として Android 2 系で動くものはほぼ動くという頭があったのです)。

頭を悩ませましたが、解決は Babel < 7 な状況であれば、トランスパイルオプション loose: true が有効的であることはわかりました。Babel >= 7 な状況であれば、下記の issue が参考になるでしょう。

// パッチとして下記のようなものが提案されています
// Android 4.x support
var defineProperty = Object.defineProperty;
Object.defineProperty = function(exports, name) {
  if (name === "__esModule") {
    exports[name] = true;
    return;
  }
  return defineProperty.apply(this, arguments);
};

このパッチを、バンドルするファイルのソースの冒頭につなげるようスクリプトを書き換えて対応しています。

こういった OS, V8, WebKit, Chromium, 端末などの固有のバグについては前述の通りすべて網羅することは人智を越えています。状況に遭遇し知見として共有する以外、対処していく方法はまずない気がしています。

最近も固有のバグに遭遇しました。Android 4.0, 4.2 で再現したバグですが、:empty で指定したスタイルが empty でなくても適用されるというバグに遭遇しました。該当しそうなのは WebKit 528 以降〜で、どこまで影響範囲かはわかっていません(もちろん現在の WebKit でこのバグは発生しない)。

<!-- 下記のコードはバグがあれば、子要素を含んだ parent が表示されません -->
<div class="parent"></div>

<style>
  .parent {
    display: block;
    min-height: 100px;
    background: #88f;
    padding: 20px 0;
    outline: 4px solid #6ff;
  }
  .parent:empty {
    display: none;
  }
</style>

<script>
  document.querySelector(".parent").innerHTML =
    '<p class="child">表示されていれば正</p>';
</script>

まとめ

  • 開発者が意識せずレガシー端末へのフォローをすることはある程度まで自動化可能
  • 固有のバグは知見でしかない、遭遇しないと分からない

ということが、全身ちぎれて血まみれになる中でわかったことです。

ただお伝えしたいのはここでご紹介した 支えなくて良い技術を支える技術 は、いずれ価値のなくなるハックです。モダンなブラウザをターゲットととしたプロジェクトにはなんの価値もありませんし、個人としても今後のキャリアにメリットはありません。

また、サービスとして決定し支えなければならないユーザがいる限り、支えなくて良い技術 => サービスとして支えるべき技術 ではあるので、フロントエンドエンジニアリングで支えてはいくものの、

  • サービスはグロースさせたい、でもスコープは変えない、といった矛盾する点
  • ユーザに届ける価値だけではなく、何より周辺環境の注意喚起を含めたユーザのリテラシー教育も業界の末端で担うという点

上記 2 点はきちんと議論すべきなのではないかなと日々考えています。