技術日誌

野球関係のサービスを個人開発しています。

共通の設定をリポジトリ化する

経緯

現在、私が運営しているサービスのうち、

野球スコア掲載プラットフォーム

キャップ野球総合サイト

この2サービス、共通のアプリケーションサーバを持っていてUIが異なるだけなのです。

画像や設定など、2つのサイト(異なるリポジトリ)間で同じものを共有したい場合、 別のリポジトリからnpm installするのがよいようです。

実際に作ってみる

今回は試合のIDとyoutubetwitterのツイートを紐づけするconfigファイルを 共通リポジトリに置きます。

今回作ったリポジトリのindex.js

export { twMovieConfig } from './config/twMovieConfig';
export { youtubeConfig } from './config/youtubeConfig';

共有したいファイルをexportする。

package.json

{
  "name": "common",
  "version": "0.0.202001131200",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "description": ""
}

インポートする側のpackage.json

 {
    "dependencies": {
    ........
        "common": "https://github.com/ckoshien/common.git"
    }
  }

課題

  • 変更があった場合は毎回npmインストール
  • package-lock.jsonを削除しないとインストールできない
  • package.jsonのバージョンを上げないと変更があったと認識されない

2019年はUI力を上げた年だった

2019年、色々なものを作ったので少しずつ振り返ろうと思います。

経緯

元々、業務系のサーバサイドのプログラムを得意としていたのもあって、WEBサービスのUIは全く無関心というかCSSの中身もよくわからない状態でした。

作ったものを振り返る

画像多めですが、駆け足で振り返ります。 UIに目覚めたのは8月のぐらいでしょうか。 10月後半からは怒涛の毎週リリース(毎週末何か作っては発表)をしていました。

1/4 slack流量計

slack流量計の新バージョンをリリースしました
運営者ギルドで使っているslackの投稿数を可視化するツールです。
あぁ、これまだ2019年でしたか....。

2月-6月

2月


2月末。試行錯誤していますが、振り返ると首を傾げたくなるデザイン...。

4月


今の野球成績管理システムはこのあたりでfixした模様。

5-6月

このあたりから野球スコア管理システムのver3を製造開始。

8/18 タイピングスコア管理システム

タイピングスコア管理システムを作った

社内向けのタイピング練習システム。このあたりから若干UI凝り始めた。

8/21 キャップ全国大会非公式サイト

キャップ野球全国大会の非公式特設サイトを作った


このサイトが事実上の転機になりました。
自分でUIを作るのに試行錯誤してCSS書きまくって...桜吹雪まで飛ばしてます。このあたりからUIの大切さとかUIへのこだわりとかが生まれた気がします。

9/9 CSSでtrans-am

CSSアニメーションでトランザム!
CSSアニメーションの作り方が何となくわかった回。

10/6 野球スコア管理システムver3

10/21 ドラフトなう!

ドラフト風画像作成サービス「ドラフトなう!」を作りました

ドラフト会議に間に合わなかったやつですね!

10/27 アニメランキング作成サービス

アニメランキング作成サービスをリニューアルしました

11/17 音楽ランキングメーカー

音楽ランキングメーカーをリリースしました

Reactのドラッグアンドドロップが気に入ったのもあってUIを流用して別カテゴリでサービスを作ってみた。

12/1 優勝ラインシミュレータ

優勝ラインシミュレーター作りました これはクソ(UI)アプリ。ちょっとしたいきさつがあってクソアプリアドベントカレンダーに載りました。

12/2 ボウリング幹事アプリ

ボウリング幹事アプリ MQTTを使った表示画面同期を実装しました。

12/8 イベントカレンダー「ふたびより」

イベントカレンダー「ふたびより」

12/11 キャップ野球チームページ作成(wix)

東京世田谷キャッパーズ

12/14 キャップ野球情報局

CMS風サイトを作りました

react-big-calendarの攻略

イベントカレンダー機能

Reactで作ったCMSにカレンダー機能を実装しました。

動作イメージ

使ったライブラリ

アジェンダテーブル

theaderとtbodyが連動していないため、OSSに初めてプルリクを出してみました。 とりあえず応急措置としてスタイルで固定で横幅を指定します。

th.rbc-header:nth-child(1){
  min-width: 77px;
}
th.rbc-header:nth-child(2){
  min-width: 121px;
}
th.rbc-header:nth-child(3){
  width: 100%;
}

表示の日本語化

import globalize from "globalize";
require("./globalize.ja.js");
const localizer = globalizeLocalizer(globalize);

globalize.culture.ja.js

localizerはreact-big-calendarにpropsとして渡します。

ツールバーの日本語化

こちらはglobalizeに対応していないハードコーディングなので、 CSSで元の文字を0pxにし、要素を増やす形で置き換えます。

/*文字置き換え*/
.rbc-btn-group > button{
  font-size: 0px;
}
.rbc-btn-group:nth-child(1) > button:nth-child(1)::before{
  font-size: 14px;
  content: '今日';
}
.rbc-btn-group:nth-child(1) > button:nth-child(2)::before{
  font-size: 14px;
  content: '←';
}
.rbc-btn-group:nth-child(1) > button:nth-child(3)::before{
  font-size: 14px;
  content: '→';
}
.rbc-btn-group:nth-child(3) > button:nth-child(1)::before{
  font-size: 14px;
  content: '月';
}
.rbc-btn-group:nth-child(3) > button:nth-child(2)::before{
  font-size: 14px;
  content: '週';
}
.rbc-btn-group:nth-child(3) > button:nth-child(3)::before{
  font-size: 14px;
  content: '日';
}
.rbc-btn-group:nth-child(3) > button:nth-child(4)::before{
  font-size: 14px;
  content: 'スケジュール';
}

props

components

event(月・週・日で表示されるコンポーネント)とagenda(スケジュールで表示されるイベント)の動作を記述できる。

Event = ({ event }) => {
    return (
      <span>
        {event.title}
      </span>);
  };

  EventAgenda = ({ event }) => {
    return (
      <div
        onClick={()=>{
          this.setState({
            isOpen:true,
            selectEvent:event
          })
        }}
      >
        {event.type}/{event.title}
      </div>
    );
  };


components={{
            event: this.Event,
            agenda: {
              event: this.EventAgenda
            }
          }}

onSelectEvent

agendaで表示されるコンポーネントが選択されたときに発火するイベント (ここではポップアップモーダルを開く)

onSelectEvent={(event,e)=>{
              this.setState({
                isOpen:true,
                selectEvent:event
              })
          }}

eventPropGetter

event.typeによってスタイルを変える処理 参考:Change color of react-big-calendar events

          
          eventPropGetter={(event, start, end, isSelected) => {
            let bgColor;
            let fontColor;
            switch (event.type) {
              case "練習会":
                bgColor = "#BF7A87";
                fontColor = "aliceblue";
                break;
              default:
                bgColor = "#386537";
                fontColor = "aliceblue";
                break;
            }
            let newStyle = {
              backgroundColor: bgColor,
              color: fontColor
            };
            return {
              className: "",
              style: newStyle
            };
          }}

キャップ野球向けイベントカレンダー作ってみました

キャップ野球向けイベントカレンダー

https://cap-calendar.netlify.com/

最近キャップ野球関係のサービスをいくつか作っているのですが、

  • キャップ野球・全国大会非公式特設サイト
  • 野球リーグスコア管理システム ver3α
  • 優勝ラインシミュレーター

まだ普及途上にあるマイナースポーツなので、色々広報手段が足りないようで「こんなイベントカレンダー欲しい」という声を聞いたので作ってみました。

検討した仕組み

マイブームというわけではないのですが、個人開発ではDBレスのサービスを作ることが多いです。スキーマの設計が面倒なのと個人でデータを持ちたくないのが....。

最近作った主なDBレスサービス

  • キャップ野球・全国大会非公式特設サイト
  • ドラフト風画像作成サービス「ドラフトなう!」
  • アニメランキング作成サービス「Annict Access 3」
  • 音楽ランキングメーカー
  • 優勝ラインシミュレーター
  • ボウリング幹事アプリ「bowling party manager」

timetree

カレンダー共有ということで一番最初に考慮したのがtimetree。 ただし、外部公開されているAPIの中にカレンダーに登録されているイベントの一覧を取得するAPIがないことがわかり(19/12/7時点)、採用を断念しました。

google calendar API

定番のカレンダー。ただし、現在公開されているv3 APIはOAuth必須となっている。極力ユーザに余計なアクションをさせたくなかったので選択肢から外しました。

google public calendar

googleカレンダーには誰でも閲覧できる「公開カレンダー」があり、 その仕組みを調べていたところ、OAuthなしでデータを取得できるという結論に達しました。

イベントの種類タグ

イベントの種類を分類するために、イベントのタイトルにタグをつけてもらう方式にしました。 例:【イベントの種類】【イベントの場所】タイトル

強敵、正規表現....

エンジニアとして避けては通れない道なのはわかっているのですが、1つめの【】の中は取得できるものの、繰り返しの取得ができず、

(ノ`Д)ノ彡 ┻━┻ ←こんな感じになりました

結局文字列を再帰的に検索するという力業で解決しました。 文字列処理はHSP時代に腐るほどやったんや....

findTag=(str,tags)=>{
    console.log(tags)
    let beginIdx = str.indexOf('【');
    let endIdx = str.indexOf('】');
    if(beginIdx !== -1 && endIdx !== -1){
      let tagStr = str.substring(beginIdx + 1,endIdx);
      tags.push(tagStr);
      this.findTag(str.substring(endIdx + 1,str.length),tags);
      return tags;
    }else{
      return tags;
    }
  }

ロースキルでごめんなさい。

採用フォント

ELLZKugUwAAB9xs.png

普段Noto Sans JPを好んで使っているのですが、Noto Sans JPのままでは若干固いイメージだったので、M PLUS Rounded 1cを使っています。

動作イメージ

未実装の機能

  • 更新日時順表示

MQTTを使ってみよう

MQTT(MQ Telemetry Transport)とは?

通信プロトコルの一種でPub/Sub型のデータ配信モデル。 軽量プロトコルのため、主にIoTの分野で使われていることが多いです。

Pub/Sub型

Pub/Sub型に関わるのは次の3者。

  • Publisher メッセージを出す人
  • Subscriber メッセージを読む人
  • Broker 配送業者(中継サーバ)

履歴の残らないLINE、という感じでしょうか。

中継サーバを立ててみよう

eclipse-mosquittoというMQTTサーバのdockerイメージが配布されています。

今回はホストの1883番ポートにmosquittoサーバを公開します。 さらにリバースプロキシの後ろに配置するため、nginxと同じnetwork(ここではdefault)に接続します。

docker-compose.yml(抜粋)

  mosquitto:
    image: eclipse-mosquitto
    hostname: mosquitto
    container_name: mosquitto
    ports:
      - "1883:1883"
    volumes:
      - ./mosquitto.conf:/mosquitto/config/mosquitto.conf
    networks: 
      - default

moquitto.conf(一部)

# ========================================================
# Default listener
# ========================================================
# Port to use for the default listener.
port 1883
# Choose the protocol to use when listening.
protocol websockets
# listener port-number [ip address/host name]
listener 9001

nginx.conf(listen 443)

 location /mqtt {
            proxy_pass  http://mosquitto:1883/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
  }

クライアント(react)側実装

import PahoMQTT from "paho-mqtt";
const uuidv4 = require('uuid/v4');

//wssを利用する
const client = new PahoMQTT.Client(
    '******',443,uuidv4()
 );
 
 export const connect=()=>{    
    client.connect({
      userName: "******",
      password: "********",
      useSSL: true,
      onSuccess,
      onFailure
    });
    client.onMessageArrived = onMessageArrived;
    client.onConnectionLost = onConnectionLost;
}

const onSuccess=()=>{
    client.subscribe(TOPIC);
}
const onFailure=()=>{
    console.log('connect failed.')
}

export const onMessageArrived=(message)=>{
    console.log(message.payloadString);
}

export const send=(message)=>{
    client.send(TOPIC, message, 0, false);
}

function onConnectionLost(responseObject) {
    if (responseObject.errorCode !== 0) {
      connect();
    }
  }

アドベントカレンダー

2日目に登板予定です。 MQTTを基礎技術に使ったアプリを作る予定です。

音楽ランキングメーカーをリリースしました。

https://music-ranking-maker.netlify.com/ - URLシェアの例

開発のきっかけ

UIが似ているのでお気づきの方もいらっしゃると思いますが、10/27にリリースしたアニメランキング作成サービスの派生プロジェクトです。 楽曲の情報も持てないかな、と思ってちょっと調べたところ、itunes search APIを見つけました。

動作イメージ

アニメランキング作成サービスとの違い

楽曲試聴機能

今回使ったライブラリ:react-player 音楽プレイヤーを実装するにあたり、いくつかのコンポーネントを検討しましたが、最初のうち、あまりにUIが簡素(開発者が自由に作れる)で敬遠していました。

ただ、controlsオプションでHTML5 Audioのネイティブプレーヤーを表示できることに気づいたのでこちらを採用しました。

JSONPの解釈

アニメランキング作成サービスではGraphQL APIを使用していましたが、 itunes search APIはRESTでGETリクエストを送るだけです。 ただし、今回はサーバ側ではなくクライアント側の実装なのでCORS問題が発生します。 クエリパラメータにcallbackを指定して、fetch-jsonpJSONPを扱います。

itunes search APIへの問い合わせ。ページングとURLエンコード済み

(async()=>{
        if(keyword !== undefined){
            let url = 'https://itunes.apple.com/search?term='+encodeURIComponent(keyword)+'&offset='+((pageNum-1)*50)+'&limit=50&media=music&country=jp&lang=ja_jp';
            let resp = await fetchJsonp(url);
            let response = await resp.json();
            if(response !== null){
                store.dispatch(loadData(response.results))
                console.log(store.getState());
            }
        }
    })().catch(
        error=>{
          console.log(error);
        }
    )

要素内スクロール

アニメランキング作成サービスではあまり使っていなかったのですが、 下部にプレイヤーが表示されるため、

.scroll{
    height: calc(100vh - ヘッダの高さ);
    over-flow: scroll;
}

スクロールを実装しています。

選択時にUIがガタガタする問題への対処

アニメランキング作成サービスではまだ改善できていませんが、 選択前と選択後で枠線の太さを変える実装をしているため、UIががたつくという問題がありました。 あらかじめ同じ太さで目立たない色の枠線にしておき、選択された際に色のみを変える方法を取っています。

NodeJSでGraphQLのサーバ側処理を実装してみる

背景

先週はクライアントからGraphQLへのアクセス方法を学習しました。

今週は「サーバサイドはどう実装するのか?」という部分を調査しました。

パッケージインストール

今回は既に実装しているアプリケーションサーバにgraphqlを組み込みます。

$ npm install graphql express-graphql -save

スキーマ定義

jsonでもtypescriptでもないので一旦外部ファイル(graphql/schema.graphql)として定義しました。 graphqlに対応しているlintとかあるみたいなのですが、 今回は調査が追いついていません。

Queryタイプに型とクエリを定義する

id(選手ID)でフィルタリングしてPlayer(選手)を返すクエリと players(選手リスト)を返すクエリを想定します。 Playerオブジェクトは以下のようなプロパティを持つとします。 DBからデータを取得して返却する際にプロパティ同士がマッピングされます。

type Query {
  player(id: Int!): Player
  players(name: String): [Player]
},
type Player {
  id: Int
  name: String
  team_name :String
  team_id: Int
}

エンドポイントの作成

  • スキーマを外部ファイルから読み込む
  • buildSchemaに渡してスキーマを生成する
  • express_graphqlに
    • 第一引数にスキーマ
    • 第二引数にメソッド定義
    • 第三引数はAPIエンドポイントでGraphiQL(GUI)を動かせるようにするかどうか

を渡す。

App.ts

import { graphql} from './graphql'
import * as express_graphql from 'express-graphql';
import { buildSchema } from 'graphql';
(中略)
let schemaStr = readFileSync('./graphql/schema.graphql',{encoding:'utf8'})
var schema = buildSchema(schemaStr);
let root = {
  player:(args)=>{
      let result = new graphql().getPlayer(args)
      console.log(result)
      return result
  }
};
app.use('/graphql', express_graphql({
  schema: schema,
  rootValue: root,
  graphiql: true
 }));

メソッド実装

GraphQLの処理部分は分けて実装したかったので、別途graphql.tsを作ってそちらに実装します。

  • App.tsで定義したGraphQLのメソッド定義を書く

graphql.ts

import { playerService } from "../services/playerService";

export class graphql{
    public getPlayer(args:any):Promise<any>{
        return new Promise((resolve,reject)=>{
            (async()=>{
                //既存のDBアクセスメソッドにリクエストパラメータを渡す
                let player = await new playerService().findId(args.id)
                console.log(player)
                resolve(player[0])
             })()
             .catch((err)=>{
                 console.log(err)
                 reject(err)
             })
        })
    }
    
}

完成イメージ(動画)

参考リンク