2020年20年目を迎える IE6 に対応した SPA を TypeScript で作る

## 現況
---
Internet Explorer 6 は 2001/8/27 にリリースされまして、ブラウザシェアは現在 0.01% となっております。
<img width="251" alt="Screen Shot 2020-03-21 at 21.58.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142668/40bb053d-ba34-baa0-e6c0-71fed03f6851.png">
ちなみに2年前に調べた時は0.1%強だったのでかなり減っています。

---
## 使ったもの
---
https://github.com/boiyama/ie6-compatible-spa-ts

- Lang: [TypeScript](https://www.typescriptlang.org/)
- UI: [Hyperapp v1](https://github.com/jorgebucaran/hyperapp)
- i18n: [i18next](https://www.i18next.com/)
- HTTP client: [jQuery 1](https://jquery.com)
- CSS: [Cascade Framework](http://jslegers.github.io/cascadeframework/index.html)
- Icon: [Material design icons](https://google.github.io/material-design-icons/)
- Hosting: [Google App Engine](https://cloud.google.com/appengine/)


---
## IE6 に対応することとは
---
- IE6 環境を用意する
- TypeScript を ES3 ベースに独自の実装を加えた JScript に変換する
- バージョン毎に実装が異なるスタイルと向き合う
- Web API を JSONP で作る
- TLS 1.0 に対応したサーバに設置する

ということです。
順番にみていきましょう。

---
## IE6 環境を用意する
---
Windows XP マシンを入手するか、[BrowserStack](https://www.browserstack.com) の有料プランを契約します。 ($29~/month)
<font color="Red">⚠ デフォルトで年間一括選択しているので気を付けないと348ドル支払ってしまいます。僕は支払ってしまいました。</font>
<font color="Red">そして自動更新なので気を付けないともう348ドル支払ってしまいます。僕は(ry</font>

---
## TypeScript を ES3 ベースに独自の実装を加えた JScript に変換する
---
### ES3に変換する
tsconfig で `"target": "ES3"` という設定ができますが、今回は使用しません。

バンドラを使うと結局バンドラがコードを結合するために書き足している部分が ES5 のため、バンドル後の JS に対して Babel の ES3 向けのプラグインを使って変換する処理を書きます。

```js
const after = babel.transformSync(before, {
  plugins: [
    "@babel/plugin-transform-member-expression-literals",
    "@babel/plugin-transform-property-literals",
    "@babel/plugin-transform-property-mutators",
    "@babel/plugin-transform-reserved-words"
  ]
}).code;
```

[Parcel](https://parceljs.org/) の場合は JSPackager を拡張して、バンドルした最終的なコードに対して上記の処理をかけます。

```js
class CustomJSPackager extends JSPackager {
  async setup() {
    // バンドルコードをES3に変換する処理を追加する
  }
}

// jsのPackagerをCustomJSPackagerに変更する
bundler.addPackager("js", CustomJSPackager);
```

[コード](https://github.com/boiyama/ie6-compatible-spa-ts/blob/master/web/scripts/bundler.js#L32-L42)

これは構文変換してるだけなので、 ES5 や ES6 などで追加されたメソッド (Array.prototype.map や Promise など) は使えませんし、全ての構文が動作するように変換されるわけでもありません。

例えば get/set 構文は get/set プロパティを持った Object.defineProperty に、 export from 構文は get プロパティを持った Object.defineProperty に変換するところまではできますが、それを正しく挙動を実現できる polyfill がありません。

---
### 外部ライブラリに対応する
今時のライブラリを使おうとすると、上記作業だけでは全く動作しません。
まずは ES5, ES6 の polyfill を入れます。

```html
<!--[if lt IE 9]>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.13/es5-shim.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.13/es5-sham.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.5/es6-shim.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.5/es6-sham.min.js"></script>
<![endif]-->
```

加えて、前述の変換できない構文を使っていないもの、という制約で選定します。
UI ライブラリでは、 IE6 で動いた実績のある [Hyperapp](https://github.com/jorgebucaran/hyperapp) を使用します。
最初 v2 で実装したのですが、エラーは出てないしHTMLも出力されてるのになぜか画面が描画されなくて、時間切れとなり残念ながら v1 で実装しています。

---
### JScript 独自実装に対応する

#### EventLister
IE8 以下は addEventLister, removeEventListener ではなく、 attachEvent, detachEvent という実装になっています。
Element.prototype.addEventLister の polyfill を使いたいところですが、
IE7 以下には Element がありません😇

対応方法として、 Document.createElement_ を作って、 addEventLister メソッドを追加し ([コード](https://github.com/boiyama/ie6-compatible-spa-ts/blob/master/web/src/document.createElement_.ts#L27-L81)) 、
バンドラで Document.createElement を Document.createElement_ に書き換えます ([コード](https://github.com/boiyama/ie6-compatible-spa-ts/blob/master/web/scripts/bundler.js#L24)) 。

#### setAttribute
IE7 以下では class 属性を setAttribute する場合、`setAttribute("class", "foo");` ではなく、`setAttribute("className", "foo");` と書かないと適用されません😇

対処方法として、 Document.createElement_ に element.setAttribute_ を追加し ([コード](https://github.com/boiyama/ie6-compatible-spa-ts/blob/master/web/src/document.createElement_.ts#L83-L101)) 、
バンドラで setAttribute を setAttribute_ に書き換えます ([コード](https://github.com/boiyama/ie6-compatible-spa-ts/blob/master/web/scripts/bundler.js#L25-L26)) 。

#### hashchange
SPA のルーティングでは History API を使用しますが、IE9 以下にはないので hashchange イベントを使って近しい挙動を実現します。
ところが、IE7 以下には hashchange イベントすらありません😇

対処方法として、 location.hash の変更を setInterval で検知する方式をとります。
Hyperapp v1 用のルーティングモジュールとして @hyperapp/router があるので、こちらを利用して History 非対応ブラウザ用に setInterval 方式を追加します ([コード](https://github.com/boiyama/ie6-compatible-spa-ts/blob/master/web/lib/%40hyperapp/router/location.ts#L90-L102)) 。


#### insertBefore
IE8 以下では、 insertBefore の第二引数には DOM element か null を渡さないとエラーが発生するようになっていて、コンポーネントを書き進めていくうちに、Hyperapp内の処理で不正な第二引数が渡されるケースがありました。

バンドラで該当の式を差し替えてしまいます ([コード](https://github.com/boiyama/ie6-compatible-spa-ts/blob/master/web/scripts/bundler.js#L27-L30)) 。


---
## バージョン毎に実装が異なるスタイルと向き合う
---
IE8 以下ではほとんどセレクタが使えないため、クラスを指定するか直接スタイルを付けていくことになるので、 CSS-in-JS できたら良いんですが、その手のライブラリで使用されている CSSStyleSheet が IE8 以下非対応なので使えません。

また、IE の CSS は各バージョン挙動が異なっているので、個別にスタイルを適用するCSSの書き方をするハックがありますが、とても骨が折れるので、大枠のパーツは [Cascade Framework](http://jslegers.github.io/cascadeframework/index.html) という古い CSSフレームワーク を使用し、手で書くところは IE6 ハックのみ使って、 IE7 以降の CSS でできる範囲のスタイルにします。

```css
.foo {
  position: fixed;
  _position: absolute; // IE6のみ適用されるプロパティハック
}
* html .foo { // IE6以下に適用されるセレクタハック
  position: absolute;
}
```

Icon は古い FontAwesome でも IE6 非対応なのですが、意外なことに [Material design icons](https://google.github.io/material-design-icons/) が対応しています。

ただし書き方に少し工夫が必要で、通常は `<i class="material-icons">face</i>` と書くところ、 IE9 以下は合字非対応なので数値文字参照で `<i class="material-icons">&#xE87C;</i>` という風に書く必要があります。

[対応表](https://github.com/google/material-design-icons/blob/master/iconfont/codepoints)


---
## Web API を JSONP で作る
---
データ取得元のAPIがクロスオリジンの場合、 CORS で通信を実現しますが、 IE7 以下は CORS 非対応です。
JSONP という、取得したいデータを引数にいれた関数を実行する js を生成する API URL を指定した script タグを動的に生成する方式で実現します。

クライアント側は IE6 と JSONP に対応した XHR ライブラリとして jQuery v1 を使用し、
サーバ側は、 Express が JSONP でのレスポンスをサポートしているので JSONP に変換するプロキシに使用します。

```js:main.js
express()
  .get("/", (_, response) =>
    p("https://api.github.com/users/boiyaa/repos?sort=updated").then(repos =>
      response.jsonp(JSON.parse(repos.body.toString()))
    )
  )
  .listen(8080);
```

クライアントサイドで以下のように書けばデータが取得できます。

```js
$.ajax({
  url: "//api-dot-boiyama-ie6-spa-ts.appspot.com",
  jsonp: "callback",
  dataType: "jsonp"
}).done(repos => {
  //     ↑取得結果
});
```

---
## TLS 1.0 に対応したサーバに設置する
---
XP の IE は SSL 2.0, SSL 3.0, TLS 1.0 にしか対応していないのと、SNI 非対応なので、今時の Netlify や Firebase Hosting には接続できません。
[Google App Engine](https://cloud.google.com/appengine/) なら上記環境に対応しつつ、Node.js アプリケーションにも、静的ファイルにも対応しています。
デプロイもちょっと設定を書いて1コマンドなので簡単です。

```yaml:SPA用のapp.yaml
runtime: php55
handlers:
  - url: /(.*\.(js|map|css|jpg|png|ico))$
    static_files: dist/\1
    upload: dist/.*\.(js|map|css|jpg|png|ico)$
  - url: /.*
    static_files: dist/index.html
    upload: dist/index.html
```

```yaml:API用のapp.yaml
runtime: nodejs10
service: api
handlers:
  - url: /.*
    script: auto
```

```sh
$ gcloud app deploy
```

---
## 完成🎉
---
デモ: https://boiyama-ie6-spa-ts.appspot.com

IE6 を持っていない方がほとんどだと思うのでスクショ残しておきます。

ホーム画面からリンクを押すと画面遷移します。
![home.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142668/b5b45cbf-c8e5-78c6-5df3-ab0429edfcc5.png)
データ取得ボタンを押すとAPIへの問い合せが始まります。
![before.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142668/38584875-4723-8ec0-7401-9ecd1430a154.png)
ロードが終わり、データが表示されます。
![after.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142668/215e782a-c317-819e-76d1-d8534e34f1c1.png)

---
## おしまい
https://github.com/boiyama/ie6-compatible-spa-ts