ブラウザに弾幕を流して匿名チャットできるやつを作った

この投稿は[クソアプリアドベントカレンダー2020](https://qiita.com/advent-calendar/2020/kuso-app) 19日目の投稿です


# 弾幕流して匿名チャットとは
投稿したメッセージがこの拡張を使ってる全員のブラウザ画面に流れます。
このカレンダーを見てるみんなが好きな💩も流し放題です。
![w9o45-g1g93.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7193/d6906075-e745-abbc-cef0-86692bf2a58b.gif)


### 気遣い
弾幕が画面に流れるのが邪魔だぜというときのために設定でON/OFFできるようにしています。
![スクリーンショット 2020-12-16 2.37.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7193/cb88fa9f-5f1d-6237-543d-20545323741d.png)

### 私はなぜ匿名チャットを作ってしまうのか
自分はわりとグズグズしたことを言いたがる人間なのですが、SNSにそういうことを書くのは自分も周りも気が滅入るし言いづらいです。ネガティブな言葉は人間をダメにする。でもしんどいときはしんどいって叫びたい。そしてそういう独り言っていうのは完全なる無に向かって叫ぶのではなく、誰かが聞いてるか聞いてないかぐらいのとこに言いたいんですよ。わかりますか？わからなくてもいいです。[2年前にも似たようなことをやったんですが](https://qiita.com/ampersand/items/968ac952ddcc8dd30b87)、まだまだクソみたいなつぶやきをどこかに吐き散らかしたいという想いは尽きなかった。


## 手元でお試しいただくにはこちらから

https://chrome.google.com/webstore/detail/barrage-chat/odlofblmifehinhbohhmmidoamogehhi?hl=ja&authuser=0

# 構成
めちゃくちゃ単純。そしてherokuはこの程度の使い方なら無料で利用できます。
socket.io用のサーバとして使うのが便利で気に入っています。
![スクリーンショット 2020-12-16 2.22.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7193/9c0c069a-453d-6889-ca38-d9ae97a8858b.png)

# サーバサイドのメッセージ送受信処理の実装
expressとsocket.ioを使用しています。
ルーム分けもせずユーザ認証もせず送られてきたメッセージを全員にぶん投げるという単純機構なのでコードはとても短いです。
ほんとにちゃんとしたチャットアプリ作るなら投稿内容のバリデーションやらをサーバサイドで行うべきですがこのアドベントカレンダーに投稿してるアプリでそんなことは気にしません（とはいえ一応XSSをされたりはしないようにクライアント側で実装しています）。

```js:server.js
'use strict';

const express = require('express');
const socketIO = require('socket.io');

const PORT = process.env.PORT || 3000;
const server = express()
  .listen(PORT, () => console.log(`Listening on ${PORT}`));
const io = socketIO(server);

io.sockets.on("connection", function (socket) {
  // クライアントからのメッセージ受信
  socket.on("c2s_chat_message", function (data) {
    let message = {
      value: data.value.trimStart(), // 先頭の空白だけ除去する
      style: data.style
    }
    // クライアントへブロードキャスト
    io.emit("s2c_chat_message", message);
  });
});
```

# Chrome拡張側の実装
## 構成
今回はこんな感じです

```
├── css
│   ├── bootstrap.min.css
│   └── popup.css
├── html
│   └── popup.html
├── icon.png
├── lib
│   ├── jquery.min.js
│   └── socket.io.js
├── manifest.json
└── scripts
    ├── background.js
    ├── keys.js ←localstrage用のkey定数を入れてる
    ├── popup.js
    └── script.js
```

## 1.popup画面を作る
まずはmanifest.jsonにpopup画面を使用することを記載します。
browser_actionの内容を以下のように設定します。

```json:manifest.json
"browser_action": {
    "default_popup": "html/popup.html"
}
```

上記で設定した画面の内容を作ります。普通のHTMLを作成するのと変わりません。

```html:popup.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <link href="/css/bootstrap.min.css" rel="stylesheet">
  <link href="/css/popup.css" rel="stylesheet">
</head>
<body>
  <header class="header">
    <h1 class="title">
      BARRAGE CHAT
    </h1>
  </header>
  <div class="container">
    <form id="messageForm">
      <div class="input-group">
        <input maxlength="60" id="message" type="text" autocomplete="off" class="form-control" placeholder="Message">
        <div class="input-group-append">
          <button id="submitMessage" class="btn btn-outline-primary" type="submit">
            ...省略...
          </button>
        </div>
      </div>
    </form>
    ...省略...
    <script src="/lib/socket.io.js"></script>
    <script src="/lib/jquery.min.js"></script>
    <script src="/scripts/keys.js"></script>
    <script src="/scripts/popup.js"></script>
</body>
</html>
```
↑のコードは諸々端折っていますが、こんな感じで拡張機能のボタンを押したときに表示されるページを作成します
![スクリーンショット 2020-12-16 3.18.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7193/51fdf8cc-a26b-eff2-692c-5db2b513a51a.png)

## 2.popup画面の操作からメッセージを送信する
メッセージ送信に関する部分のみ抜粋して記載します。jQueryめちゃくちゃ久しぶりに書きました。

```js:popup.js
let socket = io.connect("WebsocketサーバのURL。今回はherokuを使用");

(function ($) {
  const $message = $('#message');
  const $fontcolor = $('#fontcolor'); // 色の設定
  const $fontsize = $('#fontsize'); // サイズの設定
  $('#messageForm').on('submit', function (e) {
    e.preventDefault();
    let message = $message.val();
    if (!message) return;
    // ↓ここでサーバに送信
    socket.emit("c2s_chat_message", {
      value: message,
      style: {
        color: $fontcolor.val(),
        size: $fontsize.val()
      }
    });
    $message.val('');
    $message.focus();
  })

})(jQuery);

```

## 3. コンテンツスクリプトでメッセージを受け取り表示中の画面に弾幕を表示させる
2.で送信したメッセージはソケット通信がつながっている全クライアントにブロードキャストされます。
ページコンテンツに挿入するスクリプトでメッセージを受け取り、字幕となるDOM要素を作成してページ内に流します。

まず、manifest.jsonのcontent_scriptsを以下のように設定します。

```json:manifest.json
"content_scripts": [{
  "matches": ["<all_urls>"],
  "js": ["lib/socket.io.js","lib/jquery.min.js","scripts/script.js"]
}]
```
コンテンツスクリプトの処理はこんな感じで

```js:script.js
let socket = io.connect("WebsocketサーバのURL");

socket.on('s2c_chat_message', function (data) {
  // 弾幕表示するかどうかの設定を判断。後述。
  chrome.runtime.sendMessage({ method: "getDisplayBurretSetting" }, function (response) {
    if (response == 'true') {
      burretMessage(data)
    }
  });

  function burretMessage(data) {
    // 元ページ内のスタイルと競合させないためにDOMに直でスタイル指定
    let style = {
      'font-family': '"Yu Gothic Medium", "游ゴシック Medium", YuGothic, "游ゴシック体", "ヒラギノ角ゴ Pro W3", "メイリオ", sans-serif',
      'position': 'fixed',
      'top': 0,
      'left': 0,
      'z-index': 999999,
      '-webkit-text-stroke-width': '1px',
      '-webkit-text-stroke-color': 'black',
      'pointer-events': 'none'
    };

    let $span = $('<span></span>')
    $span.text(data.value);
    // emitされた文字色や文字サイズを指定
    // フォントの大きさ周りはimportantを用いて強制付与する
    $span.css({ 'cssText': '-webkit-text-fill-color: '+data.style.color+';display: block !important;font-weight : bold!important; font-size: '+data.style.size+'px!important' });
    $span.css(style);

    $('body').append($span);

    // span要素の高さがappendした後じゃないとわからないのでこのタイミングで位置調整
    $span.css({
      'left': $('body').outerWidth(), // 画面右端から登場
      'top' : rand(0, $(window).outerHeight() - $span.outerHeight()), // 縦位置は適当。ただしの上下はみ出しは防止する
      'width': data.style.size * data.value.length + 100 // フォントサイズ＊文字数＋調整幅。雑で適当。
    })

    $span.animate({
      'left': 0 - $span.outerWidth()
    },
      10000,
      'linear',
      function () {
        // 弾幕の出番が終わったら除去する
        $(this).remove();
      })
  }
  function rand(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
  }
});

```

メッセージ送受信と動作に関する部分は以上です。


## 設定内容を保存・判断する
弾幕を画面に流すかどうかをpopup画面から設定してlocalstrageに保存出来るようにしています。

一方、弾幕を流す処理判断はコンテンツスクリプト側にあります。
実行されてる場所が異なるので、popupのlocalstrageを参照することはできません。
ではどうするのかというと、バックグラウンドスクリプトからメッセージパッシングを用いて受け渡しします。

まず、manifest.jsonのbackgroundの内容を設定

```json:manifest.json
"background": {
	"persistent": false,
	"scripts": ["scripts/keys.js","scripts/background.js"]
}
```

バックグラウンドスクリプトにlocalstrageへの保存と読み出しの処理を書く

```js:background.js
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
  // 設定内容を参照
  if (request.method == "getDisplayBurretSetting") {
    sendResponse(localStorage.getItem(DISPLAY_SETTING_KEY));
  }
  // 設定内容をセット
  else if (request.method == "setDisplayBurretSetting") {
    localStorage.setItem(DISPLAY_SETTING_KEY, request.key)
    sendResponse();
  }
});
```

popup側で設定のトグルを切り替えた時にbackgroundのlocalstrageに保存しておいて

```js:popup.js
// 設定値用のcheckbox要素
const $displayBurret = $('#displayBurret');
$displayBurret.on('change', function () {
  chrome.runtime.sendMessage({ method: "setDisplayBurretSetting", key: $displayBurret.prop('checked') });
})
```

コンテンツスクリプト側からバックグラウンドスクリプトに保存された値を問い合わせて参照する、という流れで保存した設定値を共有できます

```js:popup.js
chrome.runtime.sendMessage({ method: "getDisplayBurretSetting" },
 function (response) {
  // コールバック
  if (response == 'true') {
    // 読みだしたあとの処理
  }
});
```

# まとめ

人間は弾幕を流すと楽しいということがわかりました。
久しぶりに小さいものですがモノづくりをして楽しかったです。
この近年スランプに陥っていて完成に至らずやめてしまうことが多かったのですが、クソアプリづくりはリハビリにも最高なので皆さんも是非ご参加ください。

明日は aiandrox さん、よろしくお願い致しますm(_ _)m