サーバなしで使えるハッシュタグ(#)のルーティングでSPAに挑戦してみた

## 背景 SPAのルーティングって何?
[jsnote](https://toyoharu-nishikawa.github.io/jsnote/)でサンプルコードを共有するときに, 普通のWEBページみたいにURLで渡せると便利だなと思いました. GitHubとかコード一つ一つにURLついているし, あんな感じにできたらいいなと思いました. でもどうやったらできるかわかりませんでした. サーバがテンプレートを返しているとしたら, GitHub Pages(静的ファイルだけ配信できる)で真似することはできないけど, なんとかならないかなぁと思っていました.

ネットで調べていたら, 自分がやりたいことはSPAのルーティングだと分かりました. そして2つの記事を見つけて参考にさせていただきました.

https://www.slideshare.net/ushiboy/spa-76170499
https://www.tam-tam.co.jp/tipsnote/javascript/post9911.html
これでできる気になりました.

## 選択肢
(1) サーバなし VS あり
SPAでルーティングするためには, サーバなしでいける#タグと,サーバが必要だけどURL書き換えてリダイレクトする方法がある. どちらにするか.

(2) # VS ?
サーバなしで行く場合,#タグだけでなく, ?を使ってGETのパラメータを設定することもできる.どう使い分けるか.

## 試行
(1) NginxでURL書き換えてリダイレクトすれば, #タグなしでGit Hubみたいなページを作れるんじゃないかと思っていました. でも, Git Hubのgh-pagesで公開するにはサーバは使えない.
そんな時, 上の記事で\#タグの存在を知って, サーバなしでできると分かったので, 迷わず#タグを選択しました.

サーバでURL書き換える方法は, きっとこういる感じに書けばいけると思います.
/jsnoteで来たリクエストを/にリダイレクトするサンプル(/index.htmlが呼ばれる)

```nginx:default.conf
 location / {
    root /usr/share/nginx/html;
    index index.html;
  }
  location /jsnote {
    rewrite /jsnote/(.*) / last;
  }
```

(2) # VS ?
今回, #タグをつけると, #以下を無視してHTTPリクエストを送ると知りました. でも, それってGETのパラメータを指定する ? でも同じではないかと思いました.下の二つは, SPAでルーティングするときに何が違うのでしょうか.

```
localhost/#draw=1
localhost/?draw=1
```

2通りを試行してみた結果, 次のことが分かりました.

- \#の場合  
 JavaScriptで動的に#以下を書き換えてもページがリロードされることはなく, popstateに登録したイベントだけが実行される.
- ?の場合  
JavaScriptで動的に?以下を書き換えるとページ全体がリロードされる. パラメータ変えるたびにリロードで画面がちらつく. GETのパラメータはHTTPリクエストとして送られるのだから, 当然かもしれない.

どちらでも同じページにはいけますが, やはり?はGETとしてパラメータをサーバに送る用途で使うと再確認し, #の方が画面がちらつかないし速いので#を選択しました. 

## 実践
\#以下のパラメータを扱うために4つの関数を作りました.
(1)parseParam: パラメータをパースしてMapオブジェクトに入れる.
(2)convertMapToHashString: パラメータを#タグようの文字列に戻す. (3)と(4)で使用.
(3)addParamToHash: パラメータを追加して#タグを書き換える.
(4)addParamToHash: パラメータを削除して#タグを書き換える.

```javascript

const parseParam = (hash)=>{
  if(hash){
    const param = hash.split("&")
      .reduce((pre,current)=>{
        const item = current.split("=");
        const key = item[0]
        const value = item[1]
        pre.set(item[0],isNaN(value)? value: parseFloat(value)); 
        return pre;
    },new Map());
    return param
  }
  else {
    return new Map();
  }
};

const convertMapToHashString = (map)=>{
  let paramArray = [];
  for(let [k,v] of map){
    paramArray.push(k+"="+v);
  }
  const paramString = paramArray.join("&");
  //console.log(paramString);
  return paramString 
}

const addParamToHash = (key,value)=>{
  const param = parseParam(location.hash.slice(1));
  param.set(key,value); 
  location.hash = convertMapToHashString(param) 
};

const removeParamFromHash = (key) =>{
  const param = parseParam(location.hash.slice(1));
  if(param.has(key)){
    param.delete(key)
    location.hash = convertMapToHashString(param) 
    return true;
  }
  else {
    return false;
  }
};

```

##### 使い方
parseParamを使ってparamにパラメータを代入する.

```javascript
const param = parseParam(location.hash.slice(1));
```

ユーザがパラメータを変えたときに,

```javascript
   addParamToHash("drawCheckBox",1);
```

もしくは,

```javascript
   removeParamToHash("drawCheckBox",1);
```
という感じで#タグを書き換える.

## 戻るボタンに対応
戻るボタンに対応するのが大変でした. リロードボタンを押してから, 戻るボタンを押すと挙動不審になりました.
そこで, この記事を読んでhistory APIを勉強させてもらいました.

https://uhyohyo.net/javascript/12_3.html

わかったこと
(1)パラメータ操作する内容は関数にしておいて, 最初に全部popstateに登録すると,リロード＆戻るボタンしてもそれっぽく動く.

```javascript
const jsnoteInitialize = ()=>{//やりたいこと全部書く
  keyBinding();
  fontSize();
  getCode();
  checkBox();
};

jsnoteInitialize();
window.onpopstate = jsnoteInitialize;

```

(2)以降, JavaScriptで動的に#タグを書き換えると, popstateで登録した関数が呼ばれるので, #タグを書き換える前に, 個別の関数をpopstateに登録するとよい. 自分で関数を呼ぶと2回呼ばれることになるから, 自分では呼ばずhashchangeイベントに任せる.

```javascript
window.onpopstate = keyBinding; //hashの変更前にpopstateに関数を登録
addParamToHash("keyBinding",keyElem.selectedIndex); //hashchangeで自動的にkeyBindingが呼ばれる
```


## まとめ
\#タグにパラメータを入れて, 処理内容をpopstateに登録して, 動的に#タグをJavaScriptで書き換えれば, ルーティングできる.


## 参考
- [jsnote](https://toyoharu-nishikawa.github.io/jsnote/#drawCheckBox=1&keyBinding=0&sample=sample/plotly/simple_bar.js): runボタンを押すとPlotlyで棒グラフを描きます.
- ルーティング追加したjsnoteのコードは[こちら](https://github.com/Toyoharu-Nishikawa/jsnote/blob/master/public/scripts/jsnote.mjs)