20191021のJavaScriptに関する記事は30件です。

エンジニアスタンプラリー~フロントエンド編#12

企画主旨

Frontend Developer Roadmapをひたすら巡回する企画
詳しくはこちら

今回の実施内容

静的型チェッカー
TypeScriptでかっちりしていこう。

Type Checkers

TypeScript

もはや何が公式ドキュメントかわからなくなったので、React+TypeScriptでヒットしたReact - TypeScript Deep Diveを参考にした。
実際には、React+TypeScript+MobX+Webpackという感じ。

Webpackがだいぶすっきり。

webpack.config.js
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        loader: 'ts-loader'
      },
      {
        test: /\.scss/, // 拡張子 .scss の場合
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader', // CSSをバンドルするための機能
            options: {
              url: false,
              sourceMap: enabledSourceMap,
              importLoaders: 2
            }
          },
          {
            loader: 'sass-loader', // Sassファイルの読み込みとコンパイル
            options: {
              sourceMap: enabledSourceMap
            }
          }
        ]
      }
    ]
  },

こんな感じの型を定義して、Reactのpropsに適応していく。

Types.ts
export interface _Skill {
  front: string[],
  back: string[],

}

export type _addSkill = () => void
export type _changeText = (text: string) => void
export type _togglePage = (bar: string) => void
export type _updateNews = (news: string) => void
App.tsx
import * as React from 'react';
import { observer, inject } from 'mobx-react';
import Header from './components/Header/Header';
import Content from './components/Content';
import Footer from './components/Footer';
import { _Skill, _addSkill, _changeText, _updateNews, _togglePage } from './Types';

interface Props {
  store?: Store | any
}

interface Store {
  page: string,
  news: string,
  skill: _Skill,
  inputText: string,
  addSkill: _addSkill,
  changeText: _changeText,
  updateNews: _updateNews,
  togglePage: _togglePage,
}

@inject('store')
@observer
class App extends React.Component<Props> {
  constructor(props: Props) {
    super(props);
    this.fetchJson = this.fetchJson.bind(this);
  }

  async componentDidMount() {~~~~~}

  render() {~~~~~~~}
}

export default App;

成果物

(型)安全第一。
もっと大規模で複雑なコードなら恩恵を感じることができたかも。
https://github.com/tonchan1216/WDR-frontend/tree/v12
https://tonchan1216.github.io/WDR-frontend/v12/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Qiitaの読み方

JavaScriptを学ぼうということで2019年10月から勉強開始。
プログラムを学びながら得ていった知識を、どのようにまとめていこうか迷っていたところ、こちらのサイトを見つけた。

正確にはこの記事に出会い、「Qiita」を知ることになった。
https://qiita.com/InamuraYuta/items/f76c5923f056edc795e5

「Qitta」はとても良さそうな予感がしたのでさっそく登録。
ところで「Qiita」ってなんて読むのか?

ぐーぐる先生に聞いてみたところ、

「チータ」

だそうです。
「きった」じゃなかった。調べてよかった。

こんな感じで、自分の知識を貯めていく場所としてこちらを使わせていただくことにしたので、よろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

年末まで毎日webサイトを作り続ける大学生 〜3日目 文字数カウンターを作る〜

はじめに

初めまして。
年末まで毎日webサイトを作っている者です。
今日は文字数カウンターを作りました。
扱う技術レベルは低いですが、同じように悩んでる初心者の方を勇気付けられれば幸いです。
今日は3日目。(2019/10/21)
よろしくお願いします。

サイトURL

https://sin2cos21.github.io/day3.html

やったこと

テキストのコピペで文字数を数えられるシステムを作りました。
使った言語はJavaScriptです。
昨日やった文字列操作(空白・改行取り除きシステム)が面白かったので今日も文字列系で作ってみました。こんな感じです↓
スクリーンショット 2019-10-21 22.16.06.png

こだわったところ

文字数に空白・改行を入れるか入れないか選べるようにしました。
空白・改行込みで文字数を知りたいなら「空白あり」ボタン
空白・改行なしで文字数だけ知りたいなら「空白なし」ボタンを押します。

html部分は入力欄をform+textarea、出力部分をbutton+divで作りました。

JavaScriptも昨日作ったものを少しアレンジした程度です↓

 <script>
        function text() {
            var text = document.forms.form1.input_text.value;
            var text11 = text.length;
            var target = document.getElementById("output");
            target.innerText = text11;
        }

        function text2() {
            var text2 = document.forms.form1.input_text.value;
            var text22 = text2.replace(/\s+/g, "");
            var target2 = document.getElementById("output2");
            target2.innerText = text22.length;
        }
    </script>

空白・改行ありの方は入力情報を受け取ってlengthメソッドで長さを図ります。
空白・改行なしの方は入力情報を受け取って、空白・改行を除いてからlengthメソッドで長さを図りました。

感想

文字列操作にはまってしまいました。
次はハイライトやったり、もっと驚くようなものを作っていきたいですね。
文字数をカウントするのってもっと難しいと思ってたんですけど実際はとてもシンプルでした。
これもメソッド作ってくれた方のおかげですね。
あと、相変わらずソースツリーを使ってのgit操作がうまくいきません。直接のアップロードはできるのでサイトは見れるものの、モヤモヤする...

参考

String - JavaScript | MDN
↑MDNは基礎が分かりやすいのでオススメです。

最後までお読みいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptメモ

.test

regexp.test(str)

regexp: 正規表現、str:チェックしたい文字列

与えられた文字列を検索し、その結果はtrue/falseで返す

const required = val => !!val.trim()

以下と同じ意味

const required = function (val) {
   return !!val.trim() //trim()はvalの前後の空白を取り除く
}

Object.keys

オブジェクトのキーだけを配列で取得

const object1 = {
  a: 'somestring',
  b: 42,
  c: false
};

console.log(Object.keys(object1));
// expected output: Array ["a", "b", "c"]

.find

提供されたテスト関数を満たす配列内の最初の値を返す。

var array1 = [5, 12, 8, 130, 44];

var found = array1.find(function(element) {
  return element > 10;
});

console.log(found);
// expected output: 12
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】2人プレイのディフェンスゲームを作ってみた

はじめに

JavaScriptでcanvasを使って、2人プレイのディフェンスゲームを作ってみました。

プレイはこちらから→ディフェンスゲーム

環境

  • Windows 10 home
  • Google Chrome

ルール

2人プレイのゲームになります。
黄色の玉が画面右から左へ移動します。
画面左まで移動したときに、HPが減っていき、HPが0になるとゲーム終了です。
プレイヤー1と2は黄色の玉が画面の左へ移動するのを阻止する必要があります。
プレイヤー1は赤色の玉、プレイヤー2は青色の玉をそれぞれ操作し、黄色の玉に当たることで、黄色の玉を消すことができます。
このようにして、黄色の玉の移動を阻止していきましょう!
また、それぞれ黄色の玉に当たると得点も付くようにしているので、協力し合うことも、対戦で遊ぶこともできます。

操作方法

1P 矢印キーでの移動
上に移動
下に移動
右に移動
左に移動
2P WASDキーでの移動
W 上に移動
S 下に移動
A 右に移動
D 左に移動

プログラム

index.html
<style>body{margin:0;}</style>
<canvas class="canvas">
<script>
"use strict";
{
const WIDTH = 1200; //キャンバスの横軸
const HEIGHT = 600; //キャンバスの縦軸
const HTML_CVS = document.querySelector(".canvas"); //キャンバスの領域の取得
const CANVAS = HTML_CVS.getContext("2d"); //キャンバスの描画機能を有効

class Circle{ //円クラス
  constructor(canvas,x,y,r,color){
    this.canvas = canvas;
    this.x = x;
    this.y = y;
    this.r = r;
    this.color = color;
  }

  draw(){ //描画
    this.canvas.beginPath(); //パスの初期化
    this.canvas.fillStyle = this.color;
    this.canvas.arc(this.x,this.y,this.r,0*Math.PI,2*Math.PI,true);
    this.canvas.closePath(); //パスを閉じる
    this.canvas.fill();
  }
}

class Player extends Circle{ //プレイヤークラス
  constructor(canvas,x,y,r,color){
    super(canvas,x,y,r,color);

    this.speed = 20; //プレイヤーの移動の速さ
    this.score = 0; //プレイヤーの得点

    this.up = false;
    this.down = false;
    this.right = false;
    this.left = false;
  }

  move(){ //キー操作での動き
    if(this.up && this.y-(this.r+this.speed)>=0){this.y -= this.speed;}
    if(this.down && this.y+(this.r+this.speed)<=HEIGHT){this.y += this.speed;}
    if(this.right && this.x+(this.r+this.speed)<=WIDTH){this.x += this.speed;}
    if(this.left && this.x-(this.r+this.speed)>=0){this.x -= this.speed;}
  }

  toEnemyDistance(enemy){ //敵との距離を算出
    return Math.sqrt((enemy.x-this.x)**2+(enemy.y-this.y)**2);
  }
}

class Enemy extends Circle{ //敵クラス
  constructor(canvas,x,y,r,color){
    super(canvas,x,y,r,color);
    this.y = Math.floor(Math.random()*(HEIGHT-this.r*2))+this.r;
    this.speed = 12;
  }

  move(){ //右から左への動き
    if(this.x > 0+this.r){
      this.x -= this.speed;
    }
  }
}

class ScoreLabel{
  constructor(canvas){
    this.canvas = canvas;
    this.x = 10;
    this.y = 40;
    this.hp = 20;
  }

  draw(p1,p2){
    this.canvas.fillStyle = "white";
    this.canvas.font = "30px Arial";
    this.canvas.fillText("hp : "+this.hp +" / P1 : "+p1.score+" / P2 : "+p2.score,this.x,this.y);
  }
}

class Game{
  constructor(){
    HTML_CVS.width = WIDTH;
    HTML_CVS.height = HEIGHT;

    this.PlayerRadius = 60; //プレイヤーの玉の半径
    this.EnemyRadius = 10; //敵の玉の半径
    this.frameRate = 50; //フレーム数
    this.timeCounter = 0; //タイムカウンタ
    this.intervalTime = 0.2; //敵の生成間隔時間
    this.gameflag = true;

    this.player1 = new Player(CANVAS,this.PlayerRadius,this.PlayerRadius,this.PlayerRadius,"red"); //1P
    this.player2 = new Player(CANVAS,this.PlayerRadius,HEIGHT-this.PlayerRadius,this.PlayerRadius,"blue"); //2P
    this.enemy = [];
    this.scoreLabel = new ScoreLabel(CANVAS);

    window.setInterval(()=>{ //ループ処理(フレーム数はFRAME_RATE)
      CANVAS.fillStyle = "black";
      CANVAS.fillRect(0,0,WIDTH,HEIGHT) //キャンバスを描画

      if(this.gameflag){
        this.player1.draw(); //プレイヤー1の描画
        this.player1.move(); //プレイヤー1の動き

        this.player2.draw(); //プレイヤー2の描画
        this.player2.move(); //プレイヤー2動き


        for(let i=0;i<this.enemy.length;i++){
          this.enemy[i].draw(); //敵の描画
          this.enemy[i].move(); //敵の動き
          if(this.enemy[i].x <= 0+this.enemy[i].r){
            this.enemy.splice(i,1);
            this.scoreLabel.hp--;
          }
          if(this.player1.toEnemyDistance(this.enemy[i])<=this.player1.r+this.enemy[i].r){ //距離による当たり判定
            this.enemy.splice(i,1);
            this.player1.score++;
            continue;
          }
          if(this.player2.toEnemyDistance(this.enemy[i])<=this.player2.r+this.enemy[i].r){ //距離による当たり判定
            this.enemy.splice(i,1);
            this.player2.score++;
            continue;
          }
        }
        this.timeCounter++;

        if(this.timeCounter % (this.frameRate*this.intervalTime) == 0){
          this.enemy.push(new Enemy(CANVAS,WIDTH-this.EnemyRadius,HEIGHT/2,this.EnemyRadius,"yellow"));
        }

        this.scoreLabel.draw(this.player1,this.player2);
        if(this.scoreLabel.hp <= 0){this.gameflag=false;}
        }else{alert("ゲームオーバー!");} //アラートでゲームオーバーを表示

    },1000/this.frameRate);

    window.addEventListener("keydown",()=>{ //キーボードのキーを押したときに処理
      if(event.keyCode==38){this.player1.up=true;} //上矢印キー
      if(event.keyCode==40){this.player1.down=true;} //下矢印キー
      if(event.keyCode==39){this.player1.right=true;} //右矢印キー
      if(event.keyCode==37){this.player1.left=true;} //左矢印キー

      if(event.keyCode==87){this.player2.up=true;} //Wキー
      if(event.keyCode==83){this.player2.down=true;} //Sキー
      if(event.keyCode==68){this.player2.right=true;} //Dキー
      if(event.keyCode==65){this.player2.left=true;} //A印キー
    });

    window.addEventListener("keyup",()=>{ //キーボードのキーを離したときに処理
      if(event.keyCode==38){this.player1.up=false;} //上矢印キー
      if(event.keyCode==40){this.player1.down=false;} //下矢印キー
      if(event.keyCode==39){this.player1.right=false;} //右矢印キー
      if(event.keyCode==37){this.player1.left=false;} //左矢印キー

      if(event.keyCode==87){this.player2.up=false;} //Wキー
      if(event.keyCode==83){this.player2.down=false;} //Sキー
      if(event.keyCode==68){this.player2.right=false;} //Dキー
      if(event.keyCode==65){this.player2.left=false;} //Aキー
    });
  }
}

new Game(); //ゲーム開始
}
</script>

以上で終了です!
ここまで読んでいただき、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React チュートリアルの三目並べに Redux を導入する

はじめに

React に入門する際に、三目並べを作成する公式チュートリアルに取り組む方は多いと思います。
実際に運用されている React プロダクトのほとんどは Redux と何らかのミドルウェアを併用していますが、このチュートリアルでは React 単体についてしか学べません。
当然と言えば当然なのですが、折角 React に入門したのですから、そのままの流れで Redux (と react-redux) も導入したいと考えるのが人情というものです。
という訳で、本記事ではその三目並べに Redux を導入してみます。
Redux 公式チュートリアルと併せて読んでみてください。

また、続編として redux-observable とかを導入する記事も書いたので、興味があればそちらもどうぞ。

事前準備

まず、React 公式チュートリアルのタイムトラベルの実装まで済ませましょう。
三目並べが完成すると、 Game component がいくつかの状態を持つと思います。
この状態を Redux に管理してもらいましょう。

Redux 導入をやりやすくするために、まずはファイルを分割します。
ここを参考に、以下のファイルに JavaScript コードを分割してください。

  • src/components.jsx
  • src/index.jsx

Redux / react-redux 導入

Redux とは

Redux is a predictable state container for JavaScript apps.

It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger.

You can use Redux together with React, or with any other view library. It is tiny (2kB, including dependencies), but has a large ecosystem of addons available.

Getting Started with Redux・Redux より引用

react-redux とは

React Redux is the official React binding for Redux. It lets your React components read data from a Redux store, and dispatch actions to the store to update data.

Quick Start・React Redux より引用

redux / react-redux をインストール

以下のコマンドで redux, react-redux をプロジェクトに追加します。
yarn を使用する場合は適宜読み替えてください。

npm install redux react-redux

action を追加

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().

Actions・Redux より引用
store の説明は後で出てくるので、今はまだわからなくても大丈夫です。)

三目並べで発生する action は以下の 2 つとします。

  • どこかのマスがクリックされる
  • いずれかの履歴がクリックされる

前者を表現した action を以下に示します。

src/actions.js
/*
 * action types
 */

export const CLICK_SQUARE = "CLICK_SQUARE";

 /*
  * action creators
  */

export function clickSquare(index) {
  return { type: CLICK_SQUARE, index };
}

「マスがクリックされた」という情報を表す CLICK_SQUARE action type と、「ある場所のマスがクリックされた」という action を生成する action creator が定義できました。

それでは、ここに「いずれかの履歴がクリックされた」を表す action typeaction creator を追加しましょう。
実装例はここにあります。

reducer を追加

Reducers specify how the application's state changes in response to actions sent to the store. Remember that actions only describe what happened, but don't describe how the application's state changes.

Reducers・Redux より引用

reducer の実装を以下に示します。

src/reducers.js
import { combineReducers } from "redux";
import { CLICK_SQUARE, JUMP_TO_PAST } from "./actions";

 const initialState = {
  history: [
    {
      squares: Array(9).fill(null)
    }
  ],
  stepNumber: 0,
  xIsNext: true
};

 function game(state = initialState, action) {
  switch (action.type) {
    case CLICK_SQUARE:
      const history = state.history.slice(0, state.stepNumber + 1);
      const current = history[history.length - 1];
      const squares = current.squares.slice();
      if (calculateWinner(squares) || squares[action.index]) {
        return state;
      }
      squares[action.index] = state.xIsNext ? "X" : "O";
      return {
        history: history.concat([
          {
            squares: squares
          }
        ]),
        stepNumber: history.length,
        xIsNext: !state.xIsNext
      };

     default:
      return state;
  }
}

 export const app = combineReducers({ game });

 // ========================================

 function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

initialState は Redux の state の初期値です。
ちなみにこれは Game component の state をそのまま持ってきただけです。

game()reducer です。
こちらも Game component の handleClick() とほとんど同じです。

combineReducers() は複数の reducer を 1 つにまとめるための関数です。
(今回の例では大した役割を担っていないので、あまり気にしなくて良いです。)

それでは、ここに「履歴がクリックされた」という action に対応する処理を追加しましょう。
実装例はここにあります。

store を追加

In the previous sections, we defined the actions that represent the facts about “what happened” and the reducers that update the state according to those actions.

The Store is the object that brings them together.

Store・Redux より引用
(ここはとても重要なところなので、Redux をよく知らない人は引用元をよく読むことをお勧めします。)

createStore() 関数に reducer を渡すことで store を作ることができます。

src/index.jsx
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Game } from "./components";
import { app } from "./reducers";
import "./index.css";

const store = createStore(app);
ReactDOM.render(<Game />, document.getElementById("root"));

ここで作成した store は、 container component を追加した後に使用します。

container component を追加

私がContainerと名付けたコンポーネントの特徴は以下の通りです。

  • どのように機能するか、ということと結びついてる。
  • PresentatinalコンポーネントとContainerコンポーネントの両方を内部に持つことができるが、たいていの場合は自分自身のためのDOMマークアップとスタイルを「持たない」。
  • データと挙動を、Presentationalコンポーネントや、他のContainerコンポーネントに対して提供する。
  • Fluxのアクションをcallしたり、アクションをコールバックとしてPresentatinalコンポーネンへ提供する。
  • たいていの場合は状態を持ち、データの源としての役割を担う。
  • higher order componentを用いることで生成される。例えばReact Reduxのconnect()やRelayのcreateContainer()やFlux UtilsのContainerCreate()である。

日本語訳: Presentational and Container Components より引用
Usage with React・Redux にも記述があるので、そちらも参照してみてください。)

React / Redux を使う時の container component というと、だいたいこんな感じになります。

src/containers.js
import { connect } from "react-redux";
import { clickSquare, jumpToPast } from "./actions";
import { Game } from "./components";

 const mapStateToProps = (state, ownProps) => {
  return state.game;
};

 const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    handleClick: index => {
      dispatch(clickSquare(index));
    },
    jumpTo: () => {}
  };
};

 export const GameContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(Game);

mapStateToProps() は、Redux の state を props として適当な形に整形する関数です。

mapDispatchToProps() は、Redux の dispatcher を props として適当な形に整形する関数です。
actionstore に渡すことを dispatch と呼び、それをする関数のことを dispatcher と呼びます。)

connect()() は、Redux の state と React component を接続する関数です。
mapStateToProps ないし mapDispatchToProps と React component を渡すと、container component が返ってきます。

これで Game component は GameContainer component から Redux の諸々を props 経由で受け取ることができるようになりました。
なので、それらを使うよう書き換えましょう。

src/components.jsx
export class Game extends React.Component {
  render() {
    const history = this.props.history;
    const current = history[this.props.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ? `Go to move #` + move : "Go to game start";
      return (
        <li key={move}>
          <button onClick={() => this.props.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

    let status;
    if (winner) {
      status = "Winner: " + winner;
    } else {
      status = "Next player: " + (this.props.xIsNext ? "X" : "O");
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={i => this.props.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}

これでこの component は内部に state を持たないし、外部の state (つまり Redux の state )の存在も知らないものになりました。
アプリケーションの状態と完全に切り離すことができたので、この component はテストをしやすいはずです。

さて、先ほど提示した mapDispatchToProps() が返却している jumpTo プロパティには不足があります。
これを完成させましょう。
実装例はここにあります。

container component と Redux の state を接続

それでは、今まで Game component を呼び出していた箇所を GameContainer に書き換えましょう。

src/index.jsx
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Game } from "./components";
import { app } from "./reducers";
import "./index.css";

const store = createStore(app);
ReactDOM.render(<GameContainer />, document.getElementById("root"));

次に、 Provider component を導入します。

The option we recommend is to use a special React Redux component called <Provider> to magically make the store available to all container components in the application without passing it explicitly. You only need to use it once when you render the root component:

Usage with React・Redux より引用

src/index.jsx
import React from "react";
import { render } from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { app } from "./reducers";
import { GameContainer } from "./containers";
import "./index.css";

const store = createStore(app);

render(
  <Provider store={store}>
    <GameContainer />
  </Provider>,
  document.getElementById("root")
);

Provider component は配下にいる container component に redux statedispatcher を良い感じに渡してくれます。
これで三目並べに Redux を導入できたはずです。

ここに、以下のような変更を加えても良いでしょう。

  • calculateWinner()src/utils.js に切り出す
  • 全ての React component を functional component にする
  • Game component の render() 内で定義されている current および statusmapStateToProps() 内に移す
  • Board component に渡されている onClick() および squares props を Redux の state から直接受け取るようにする

これで Redux の導入が完了しました。
ちなみに、導入の全容はこのリポジトリにあります。

更なる発展

プロダクトとして使うには(場合によりますが)まだ不十分です。
例えば以下のようなものが必要でしょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

異なるドメイン間でlocalstorageを共有する方法

はじめに

localStorageは基本的に同じドメイン内(厳密にはポート番号なども一致している必要がある)でしか有効ではありませんが、postMessage API(Web Messaging API)を使用することでクロスドメインでも共有することができます。

調べてみても古い記事が多かったり、サンプルのコードが無かったりだったので自分用の備忘録も兼ねてやり方をご紹介します。

構成

【ドメインA】
・localstorageのデータを持つ側。
・ドメインBからの要求に応じてlocalstorageの操作を行う。

【ドメインB】
・ドメインAに保存されているlocalstorageの値の参照や更新を要求する側。

(実際の動作確認はA側をGithub Pages、B側をローカルサーバーに置いた状態で行いました。)

ドメインA側

ドメインB側からのメッセージをトリガーにlocalstorageの操作を行っています。
(厳密にメッセージの中身を検証して処理を切り分けたりまではしていません。)

index.html
<body>
  <script>
    (function() {
      var origin = 'http://B.com';
      window.addEventListener('message', function(event) {
        // 送信元が指定のオリジンと一致していれば処理を行う
        if(event.origin === origin) {
          var message = event.data;

          // メッセージが'get'ならlocalstorageの値を返す
          if(message === 'get') {
            var storageData = localStorage.getItem('test');
            event.source.postMessage(storageData, event.origin);
          }
          // getでなければメッセージを分割してlocalstorageに保存する
          else {
            var messageArray = message.split(',');
            var key = messageArray[0];
            var value = messageArray[1];
            localStorage.setItem(key, value);
          }
        }
      });
    })();
  </script>
</body>

ドメインB側

display:noneで非表示にしたiframeでドメインA側のindex.htmlを読み込み、iframeObject.contentWindowでWindowオブジェクトを取得して、それに対してpostMessageでメッセージを送っています。

index.html
<body>
  <iframe id="iframe" src="http://A.com/index.html" style="display: none;"></iframe>
  <input type="text" name="text" id="text">
  <button id="set">ストレージに値を入れる</button>
  <button id="get">ストレージの値を取得する</button>
  <script>
  (function() {
    var iframeWindow = document.querySelector('#iframe').contentWindow;
    var $text = document.querySelector('#text');
    var $setButton = document.querySelector('#set');
    var $getButton = document.querySelector('#get');
    var origin = 'http://A.com';

    window.addEventListener('message', function(event) {
      // 送信元が指定のオリジンと一致していれば処理を行う
      if(event.origin === origin) {
        alert(event.data);
      }
    });

    $setButton.addEventListener('click', function() {
      // テキストエリアに入力されている値を送信
      iframeWindow.postMessage(`test,${$text.value}`, origin);
    });

    $getButton.addEventListener('click', function() {
      // localstorageの値の取得を要求するメッセージ送信
      iframeWindow.postMessage('get', origin);
    });

  })();
  </script>
</body>

以上になります。
もう少しややこしいロジックが必要かと思っていましたが、postMessage APIのおかげで意外と楽にできました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LIFF v2でLINEログイン、QRスキャン、LIFFからメッセージ送信などを試すハンズオン #ヒーローズリーグ

(もしかしたらLIFF v2ハンズオンは国内初かもしれない。)

この資料について

福井ハッカソン@ #ヒーローズリーグ 2019 by MAのハンズオンで実施した内容になります。

自己紹介

LIFFとは

LINE Frontend Frameworkの略で、LINEアプリの内部ブラウザ上でLINEの情報を活用したWebアプリケーションを動作させることができるプラットフォームの事を指します。
LIFF環境上で動作するWebアプリケーションのことをLIFFアプリと呼びます。

公式文面: https://developers.line.biz/ja/docs/liff/overview/

LIFFアプリはこんな雰囲気です。↓

Messaging APIの新機能LIFFの使い方を解説します。

これまでのLIFFで出来たこと

既存のLIFFでは大まかに以下の機能が扱えます。

  • ユーザーの情報取得
  • LINEにメッセージ送信
  • 外部のBLEデバイスにアクセス(ただしこれはLINE Thingsという機能になり、ちょっと別カテゴリ感なので割愛)

現状見ている限りだと、v1とv2で特段何かが出来なくなったって感じではなさそうです。
なのでこの辺の機能は

LIFF v2のアップデート

2019/10/16のリリースLIFF v2がリリースされました。

大まかにアップデートされた内容は

1. 外部ブラウザでもLIFFアプリを扱うことが出来るようになった

→ 今までのLIFFアプリはLINEのブラウザ上からしかアクセス出来ませんでした。

2. LINEログインが扱えるようになった

→ 今までのLIFFアプリはLINEのブラウザ上からしかアクセス出来なかったので、ログインはしている前提だったのですが、v2から外部ブラウザでも扱えるようになったことにより、 外部ブラウザからのアクセス時のみ LINEログインを扱うことができます。

3. QRコードスキャンが扱えるようになった

→ LIFF上からLINEアプリのQRコードスキャン機能を呼び出せるようになりました。

4. LIFFアプリを動作環境を細かく取得できるようになった

→ 外部ブラウザでもLIFFアプリが動くようになったので、外部ブラウザでの動作なのか、LINEアプリ内での動作なのかなど、動作環境を細かく取得できるようになりました。

ハンズオン

今回は これらの機能を丸っと触れてみるハンズオンになります。
また、管理のしやすさなどを加味してVue.jsを利用しています。

事前準備

  1. LINE BOTを作ったことがある
  2. Node.jsが動く環境がある
  3. ngrokが動作する環境がある
  4. ローカルサーバーを起動する環境がある
    • VSCodeのLive Serverプラグインが便利です

1~3までは「1時間でLINE BOTを作るハンズオン」をやっておくと良いです。

今回の到達目標

1 or 2で選んで行きましょう。前での紹介としては2で進んで行きますが、1のフォローもします。

  1. LINE BOTを作ったことが無い人 -> 「1時間でLINE BOTを作るハンズオン」を試してLINE BOTを作ってみるのもOK(STEP3まで)
  2. LINE BOTを作ったことがある人 -> このページをこのままお進み下さい。

1. ngrokなどでトンネリング

ngrok http <利用するポート名>という形で起動させておきます。

$ ngrok http 5500

生成されるhttps://xxxxxx.ngrok.ioのアドレスをコピーしておきましょう。次の手順で利用します。

補足

ngrokがインストール出来なさそうな人はserveoを利用すると良さそうです。特にインストールなどせずにsshでトンネリングが出来ます。Node.jsやnpmがそもそも入ってないよーって人はこちらの手段でもOKです。

servioの利用

$ ssh -o ServerAliveInterval=60 -R <利用したいドメイン名>:80:localhost:<ポート> serveo.net

実際はこんな感じ

$ ssh -o ServerAliveInterval=60 -R n0bisuke:80:localhost:3000 serveo.net
Forwarding HTTP traffic from https://n0bisuke.serveo.net
Press g to start a GUI session and ctrl-c to quit

この場合は次の手順で設定するエンドポイントURLはhttps://xxxxxx.serveo.netを設定しましょう。

2. LIFFアプリの登録

LINE DevelopersのLINE BOTの管理画面からLIFFのタブを選択し、追加ボタンを押しましょう。

エンドポイントURL先ほどのngrokのアドレスを指定し、キャプチャのようにチェックをして追加するを押しましょう。

作成したLIFFアプリケーションを確認し、LIFF URLline://app/xxxxxxxxのxxxxxxxxをメモしておきましょう。

3. 外部ブラウザでの挙動とLINEログインを試す

まずは外部ブラウザでの挙動とLINEログインを試していきます。

3.1 コードを書いていく

任意のフォルダを作成し、その中にindex.htmlscript.jsを作成しましょう。

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
        <title>LIFF v2 ハンズオン</title>
    </head>
    <body>
        <div id="app">
            <button @click=logout>ログアウトする</button>

            <h1>{{displayName}}</h1>
            <p>{{userId}}</p>
            <p>{{statusMessage}}</p>
            <img :src=pictureUrl alt="profile" width="200px" />
        </div>

        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script>
        <script src="script.js"></script>
    </body>
</html>
script.js
'use strict';

const app = new Vue({
    el: '#app',
    data: {
        displayName: '',
        userId: '',
        statusMessage: '',
        pictureUrl: '',
    },

    methods: {
        //プロフィール取得関数
        getProfile: async function(){
            const accessToken = liff.getAccessToken();
            const profile = await liff.getProfile();
            this.displayName = profile.displayName; //LINEの名前
            this.userId = profile.userId; //LINEのID
            this.pictureUrl = profile.pictureUrl; //LINEのアイコン画像
            this.statusMessage = profile.statusMessage; //LINEのステータスメッセージ
        },

        //ログアウト処理の関数
        logout: async function(){
            if (liff.isLoggedIn()){
                alert('ログアウトします。');
                liff.logout();
                window.location.reload();
            }
        },

    },

    //ページを開いた時に実行される
    mounted: async function(){
        await liff.init({
            liffId: 'xxxxxxxxxxx' // ! 先ほどメモしたものを入力してください。
        });

        //LINE内のブラウザかどうか
        if(liff.isInClient()){
            alert('LINE内のブラウザ');
            this.getProfile(); //LINE内で開いた場合は特にログイン処理なしで大丈夫
        }else{
        //外部ブラウザかどうか
            if(liff.isLoggedIn()){
                alert('外部ブラウザ');
                this.getProfile();
            }else{
                liff.login();
            }
        }
    }
});

この状態でindex.htmlでローカルサーバーを起動させましょう。

最初の手順でngrokでトンネリングさせているポート(5500)を指定して起動してください。

3.2 Webサイトにアクセスして試してみる

https://xxxxx.ngrok.ioのアドレスをPC上のブラウザなどから開いてみましょう。

こんな感じでログイン画面に遷移します。

無事にログインできるとこんな感じでアラートが出ます。

3.3 ここで繋がらない人FAQ

  • LIFFのIDちゃんとありますか?
    • ソースコード上で一箇所だけ書き換えがあるので注意
  • http://localhost:5500みたいなアクセスしてませんか?
    • https://xxxx.ngrok.ioのアドレスで

3.4 LINEアプリ内からもアクセスしてみる

LINEアプリからline://app/xxxxxxxxxxxのアドレスにアクセスしてみましょう。
何かしらの方法でLINEのBOTや会話などにこのアドレスを投稿して試してみるのが良いです。

4. QRスキャンとメッセージ送信を試してみる

QRとメッセージ送信を試します。

4.1 コードを書く

index.htmlscript.jsを更新します。

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
        <title>LIFF v2 ハンズオン</title>
    </head>
    <body>
        <div id="app">
            <button @click=logout>ログアウトする</button>

            <button @click=sendMessage>メッセージ送信</button>
            <button @click=QR>QRスキャン</button>

            <h1>{{displayName}}</h1>
            <p>{{userId}}</p>
            <p>{{statusMessage}}</p>
            <img :src=pictureUrl alt="profile" width="200px" />
        </div>

        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script>
        <script src="script.js"></script>
    </body>
</html>
script.js
'use strict';

const app = new Vue({
    el: '#app',
    data: {
        displayName: '',
        userId: '',
        statusMessage: '',
        pictureUrl: '',
    },

    methods: {
        //プロフィール取得関数
        getProfile: async function(){
            const accessToken = liff.getAccessToken();
            const profile = await liff.getProfile();
            this.displayName = profile.displayName; //LINEの名前
            this.userId = profile.userId; //LINEのID
            this.pictureUrl = profile.pictureUrl; //LINEのアイコン画像
            this.statusMessage = profile.statusMessage; //LINEのステータスメッセージ
        },

        //ログアウト処理の関数
        logout: async function(){
            if (liff.isLoggedIn()){
                alert('ログアウトします。');
                liff.logout();
                window.location.reload();
            }
        },

        //QRコードの利用の関数
        QR: async function(){
            if(!liff.isInClient()) {
                alert('LINEから開いて下さい');
                return;
            }
            //QR読み込み
            const res = await liff.scanCode();
            const msg = `読み取ったコードの中身は「${res.value}」です`;
            alert(msg);
        },

        //LINEにメッセージ送信の関数
        sendMessage: async function(){
            if(!liff.isInClient()) {
                alert('LINEから開いて下さい');
                return;
            }
            //メッセージ送信
            await liff.sendMessages([
                {
                  type:'text',
                  text:'Hello, World!'
                }
            ]);

            alert('メッセージを送信しました。');
        }
    },

    //ページを開いた時に実行される
    mounted: async function(){
        // alert(liff.getOS());
        await liff.init({
            liffId: 'xxxxxxxxxx' // ! 先ほどメモしたものを入力してください。
        });

        //LINE内のブラウザかどうか
        if(liff.isInClient()){
            console.log('LINE内のブラウザ');
            this.getProfile(); //LINE内で開いた場合は特にログイン処理なしで大丈夫
        }else{
        //外部ブラウザかどうか
            if(liff.isLoggedIn()){
                console.log('外部ブラウザ');
                this.getProfile();
            }else{
                liff.login();
            }
        }
    }
});

4.2 LINEアプリからアクセスして試してみる

この機能は外部ブラウザからのアクセスでは利用できないので、LINEアプリからline://app/xxxxxxxxxxxのアドレスにアクセスして試してみましょう。

5 チャレンジ課題

時間が余った人はチャレンジしてみましょ!

5.1 公式ドキュメントを参考に送信するメッセージをカスタマイズしてみよう

参考: https://developers.line.biz/ja/reference/liff/#send-messages

5.2 QRコードで読み込んだ文字列をLINEに送信してみよう

組み合わせチャレンジです。


まとめ

お疲れ様でした。LIFF v2の機能を試すハンズオンを行いました。
目玉機能はLINEログインとQRスキャンだと思うのですが、個人的には外部ブラウザでLIFFの活用が出来るようになったというところだと思ってます。

既存のWebサービスにもLIFFアプリの機能を活用できるポイントも増えてくるかもしれないので、LINE Thingsのように、LINEがスマートフォンアプリやチャットプラットホームからさらに活用したユースケースが出てくると思います。

みんなで活用考えてみましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MediaRecorderは便利だけどまだちょっと辛い

ブラウザで音声や映像を記録するためのAPIとして
MediaRecorderがあります。
これを使うと、音声や映像をデバイスからサクっと取ってBlobに出来ます。

const media = await navigator.mediaDevices.getUserMedia({ audio: true });
      const recorder = new MediaRecorder(media);
      recorder.addEventListener('dataavailable',(event:BlobEvent) => {
        const data:Blob = event.data;
        //Blobを使った処理
      })
      recorder.start();

こんな感じにサクっとカメラやマイクのデータが取れます。

が、色々厳しいところがまだあります。

typescriptの型定義が追い付いていない

実は上記のtypescriptコード、そのままだとコンパイルが通りません。
MediaRecorderやBlobEventが標準ではまだ存在していないので自分で足してやる必要があります。

非対応ブラウザがあるのでpolyfillが必須

上記のMDNを見ても、IEどころかEdgeが真っ赤ですし、?になっているiOSのsafariも普通にダメでした。
ついでに、この辺りのpolyfillに標準でtypescriptの型定義が付いているのがなかったので、
結局そこは自分で頑張るかanyでごまかすしかありません。
(型が付いているextendable-media-recorderというものもあったものの、こちらは逆にクロスブラウザ対応していない)

まとめ

  • 便利だけどカバーされていない部分も多いので注意
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueとReact(Hooksも)をアニメーション実装から比較する

Vueで、アニメーションするコンポーネントを作ったので、
ついでにReactでも作ってみると実装方法が違ったので比較する

作ったものは、ハンバーガーボタン(押したらバツになるやつ)
svgをGSAPのTweenMaxでclickイベントをトリガーにアニメーションさせることでハンバーガーボタンを作成する

とりあえずsvgをただTweenMaxでアニメーション

DOMを取得してTweenMaxでアニメーションする例
ボタンを押せばアニメーション

See the Pen SvgTween by Saito Takashi (@7_asupara) on CodePen.

Vueで作成する

See the Pen VueHamburger by Saito Takashi (@7_asupara) on CodePen.

Vue
<template>
  <div class="button" v-on:click="toggle"> <!-- クリックイベントを付与 -->
    <svg :viewbox="viewbox" :width="size" :height="size" style="overflow: visible">
      <!-- svgの各属性に変数をバインディング -->
      <line
        x1="0"
        :y1="line1Y1"
        :x2="size"
        :y2="getTopLimit()"
        :stroke="stroke"
        :stroke-width="strokeWidth"
      />
      <line
        :x1="line2X1"
        :y1="halfSize"
        :x2="size"
        :y2="halfSize"
        :stroke="stroke"
        :stroke-width="strokeWidth"
      />
      <line
        x1="0"
        :y1="line3Y1"
        :x2="size"
        :y2="getBottomLimit()"
        :stroke="stroke"
        :stroke-width="strokeWidth"
      />
    </svg>
  </div>
</template>

<script>
export default {
  data() { // dataオブジェクト 変更を通知したい変数とかはここで定義
    return {
      size: 50,
      stroke: 'black',
      strokeWidth: 6,
      speed: 0.4,
      line1Y1: 0,
      line2X1: 0,
      line3Y1: 0,
      menuCloseFlg: false
    };
  },
  computed: { // 加工した返り値の変数を作りたい場合はここで定義
    viewbox: function () {
      return `0 0 ${this.size} ${this.size}`
    },
    halfSize: function () {
      return this.size / 2
    }    
  },
  mounted () { // mountedではcomputedが動かないのでmethodで初期化
    this.line1Y1 = this.getTopLimit()
    this.line3Y1 = this.getBottomLimit()
  },
  methods: {
    getTopLimit () {
      return this.strokeWidth / 2
    },
    getBottomLimit () {
      return this.size - (this.strokeWidth / 2)
    },
    toggle () { // クリックイベント
      if (this.menuCloseFlg) {
        TweenMax.to(
          this.$data,
         this.speed,
          {
            line1Y1: this.getTopLimit(),
            line2X1: 0,
            line3Y1: this.getBottomLimit(),
            ease: Expo.easeIn
          }
        )
      } else {
        TweenMax.to(
          this.$data,
          this.speed,
          {
            line1Y1: this.getBottomLimit(),
            line2X1: this.size,
            line3Y1: this.getTopLimit(),
            ease: Expo.easeIn
          }
        )
      }
      this.menuCloseFlg = !this.menuCloseFlg
    }
  }
}
</script>

Vueでは、dataオブジェクトを変更すると、自動で画面にも反映(rerender)してくれる
なので、svgの動かしたい属性にdataオブジェクトのプロパティを付与してその値を変更すれば勝手に画面に反映してくれる

この例の場合は、toggleメソッドでDOMではなく、svg属性に割り当てたdataオブジェクトの値を直接TweenMaxで変更してアニメーションさせている
この特性のおかげで値の変更が直感的にできるので、アニメーションを扱う上でとてもVueはいいと思う

svgを使用した動的なUIが簡単に作れそう

Reactで作成する

とりあえずClassComponentを使って作成する

See the Pen ReactHamburger by Saito Takashi (@7_asupara) on CodePen.

React
import React from 'react';

class App extends React.Component {
  constructor(){
     super();
     // 必要な変数の定義
     this.size = 50;
     this.speed = 0.4;
     this.strokeWidth = 6;
     this.halfSize = this.size / 2;
     this.halfStrokeWidth = this.strokeWidth / 2;
     this.topLimit = this.halfStrokeWidth;
     this.bottomLimit = this.size - this.halfStrokeWidth;
     this.viewbox = `0 0 ${this.size} ${this.size}`;
     this.stroke = 'black'

     // 変更を通知したい変数はここでstateとして定義
     this.state = { closeFlg: false };

     // DOMノードを取得するための準備
     this.line1Ref = null;
     this.line2Ref = null;
     this.line3Ref = null;
  }

  handleClick = () => {
    // svgの属性ではなく、DOMノードを直接Tweenさせる
    if (!this.state.closeFlg) {
      TweenMax.to(this.line1Ref, this.speed, { attr: { y1: this.bottomLimit }, ease: Expo.easeIn })
      TweenMax.to(this.line2Ref, this.speed, { attr: { x1: this.size }, ease: Expo.easeIn})
      TweenMax.to(this.line3Ref, this.speed, { attr: { y1: this.topLimit }, ease: Expo.easeIn })
    } else {
      TweenMax.to(this.line1Ref, this.speed, { attr: { y1: this.topLimit }, ease: Expo.easeIn })
      TweenMax.to(this.line2Ref, this.speed, { attr: { x1: 0 }, ease: Expo.easeIn })
      TweenMax.to(this.line3Ref, this.speed, { attr: { y1: this.bottomLimit }, ease: Expo.easeIn })
    }

    // Reactでは変更の通知はsetStateが必須
    this.setState(prevState => ({
      closeFlg: !prevState.closeFlg,
    }))
  }

  // svgのlineタグにrefを付与してDOMノードを取得する
  render() {
    return(
      <div className="button" onClick={this.handleClick}>
        <svg viewBox={this.viewbox} width={this.size} height={this.size} style={{ overflow: 'visible' }}>
          <line
            ref={ c => this.line1Ref = c}
            x1="0"
            y1={this.topLimit}
            x2={this.size}
            y2={this.topLimit}
            stroke={this.stroke}
            strokeWidth={this.strokeWidth}
          />
          <line
            ref={ c => this.line2Ref = c}
            x1="0"
            y1={this.halfSize}
            x2={this.size}
            y2={this.halfSize}
            stroke={this.stroke}
            strokeWidth={this.strokeWidth}
          />
          <line
            ref={ c => this.line3Ref = c}
            x1="0"
            y1={this.bottomLimit}
            x2={this.size}
            y2={this.bottomLimit}
            stroke={this.stroke}
            strokeWidth={this.strokeWidth}
          />
        </svg>
      </div>
    );
  }
}

Reactでは、変数(state)変更の通知をsetStateを使って行って初めて画面に反映(rerender)される
(Reactはstate変更の通知をするかどうかをプログラマーがコントロールしたいので、setStateを実行することを採用している)
なので、Vueのように値を変更するだけでは画面に反映されない

TweenMaxのようなトゥイーン系のライブラリはフレーム毎の値の変更をループでよしなにやってくれるが、svgの属性値の変更をする場合、この中にsetStateをねじ込むことができないので変更の通知ができなくアニメーションされないはず
そこで、ReactでTweenしたい場合は、Refを使用してDOMノードを取得しDOMに対してTweenMaxでアニメーションする(要はjQueryとかと同じで昔ながらの方法)

Vueより手間が多くなり、複雑なアニメーションはめんどくさそうだ

ReactHooksで作成する

Reactでは、ClassComponentが滅びてReactHooksとかいうのを使うのがスタンダードになるらしいのでこいつのもついでに作ったが、結構めんどくさかった
ReactにはFunctinalComponentとClassComponentがあって、ClassComponentでしかstateが利用できなかったが、FunctinalComponentでもReactHooksを利用してstateを扱えるようになったらしい

See the Pen ReactHooksHambergur by Saito Takashi (@7_asupara) on CodePen.

ReactHooks
import React from 'react';

function App() {
  const size = 50;
  const speed = 0.4;
  const strokeWidth = 6;
  const halfSize = size / 2;
  const halfStrokeWidth = strokeWidth / 2;
  const topLimit = halfStrokeWidth;
  const bottomLimit = size - halfStrokeWidth;
  const viewbox = `0 0 ${size} ${size}`;
  const stroke = 'black'

  // React.useStateで、stateのgetterとsetterの定義
  // const [getter, setter] = React.useState(デフォルト値)
  const [closeFlg, setCloseFlg] = React.useState(false);
  const [clicked, setClicked] = React.useState(null);

  // useRefでDOMノードの取得 ClassComponentとだいたい同じ
  const line1Ref = React.useRef(null);
  const line2Ref = React.useRef(null);
  const line3Ref = React.useRef(null);

  // クリックイベント closeFlgをトグルするだけ
  const toggle = () => {
    setCloseFlg(!closeFlg);
  };

  // useEffect SideEffect(副作用?)を実行するやつ
  React.useEffect(() => {
    if (closeFlg) {
      TweenMax.to(line1Ref.current, speed, { attr: { y1: bottomLimit }, ease: Expo.easeIn })
      TweenMax.to(line2Ref.current, speed, { attr: { x1: size }, ease: Expo.easeIn})
      TweenMax.to(line3Ref.current, speed, { attr: { y1: topLimit }, ease: Expo.easeIn })
    } else {
      TweenMax.to(line1Ref.current, speed, { attr: { y1: topLimit }, ease: Expo.easeIn })
      TweenMax.to(line2Ref.current, speed, { attr: { x1: 0 }, ease: Expo.easeIn})
      TweenMax.to(line3Ref.current, speed, { attr: { y1: bottomLimit }, ease: Expo.easeIn })
    }
  }, [closeFlg]);

  return (
    <div className="button" onClick={toggle}>
      <svg viewBox={viewbox} width={size} height={size} style={{ overflow: 'visible' }}>
        <line
          ref={line1Ref}
          x1="0"
          y1={topLimit}
          x2={size}
          y2={topLimit}
          stroke={stroke}
          strokeWidth={strokeWidth}
        />
        <line
          ref={line2Ref}
          x1="0"
          y1={halfSize}
          x2={size}
          y2={halfSize}
          stroke={stroke}
          strokeWidth={strokeWidth}
        />
        <line
          ref={line3Ref}
          x1="0"
          y1={bottomLimit}
          x2={size}
          y2={bottomLimit}
          stroke={stroke}
          strokeWidth={strokeWidth}
        />
      </svg>
    </div>
  );
}

Refの使い方とstateをsetしないといけないのはだいたいClassComponentと同じ

ただ、クリックイベントを普通にFunctionalComponentのメソッドとして定義してもライフサイクルが考慮されないのか、そこでDOMノードにアクセスしてTweenしようとしても何も実行されない(画面にレンダリングされる前に定義されるからかな?よくわからん)

ReactHooksでは、useEffectとかいうのがComponentのライフサイクル(ClassComponentでいうcomponentDidMountとかcomponentDidUpdateとか)を管理しているみたいなので、これを利用する
クリックイベントには、stateのcloseFlgのトグル処理のみ記述し、
useEffectで実行したい処理(第一引数)と監視するstate(第二引数)を指定し、closeFlgが変更されたら実行されるようにする(componentDidUpdateにあたるかな, VueならWatcher使えば同じような実装になるような)

アニメーションに限定していえば、慣れたらいけるかもやけど全然直感的じゃないのでめんどくさく感じたし、useEffectがなんか慣れない

まとめ

両者を比較すると、Vueに比べてReactはデータを厳格に扱うことを目指していると思われる
その分、Vueは今回の場合に限らず直感的にコードが書けると思う

ページ数が小規模でアニメーションが多めのインタラクティブなLP、コーポレートサイトが作りたければVueを使うべきだと思う
一方Reactは、大規模なシステム等でデータを厳格に扱いたい場合は優位だと思う
これらの中間のものは好きな方を勝手に選ぼう

ただ、今回はsvgのTweenでアニメーションしたので違いがでたが、CSSとか代替の方法もあると思うので楽な方法を検討すればいい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ガチ調査版::2019年プログラミング言語 求人人気ランキング

背景

実求人をクロールし、どの言語がどれだけ求人を保有しているか実数を取得し、年収別の求人数から総合ランキングを作成してみました。個人の恣意的な価値観を反映しないよう、エンジニアとしての個人的な主観は可能な限り省いています。(解説のところで少し主観が入っているのでお気をつけください)

調査方法

Web上にある求人サービスから実求人をクローリングし、言語の頻出数から人気言語のランキングを調査しました。

クローリングとは何か

クローラーとは、ザックリ言うと、web上でデータを集めてくれるロボットです。webにある色々なサイトを飛び周り、こちらの命令(求めているもの)に該当するページで、データを集めてくれます。集まったデータは、各項目ごとに分別され、それぞれ値が抽出されます。抽出されたものは、何かうまいことやってデータベースに格納するなどします。

初心者でも分かる説明

水泳帽をかぶったロボットがプールの中をクロールで泳ぎまくり、「おとな20人」「こども12人」「おとこ20人」「おんな11人」「せいべつふめい1人」みたいな感じで情報、データを集めてくれる便利で良い奴です。

対象データ

調査した実求人総数は10万件ぐらい。
プログラミング言語はWikipediaの一覧から取得し、求人数が100件に満たないものは除外しました。

ref https://ja.wikipedia.org/wiki/プログラミング言語一覧

では早速見てみましょう。

年収別ランキング(絶対数順)

スクリーンショット 2019-10-21 16.49.35.png

全体的にJava,PHP,JavaScriptが上位に位置しています。特にJavaの案件がダントツに多いことが分かりますね。

この3つの言語は幅広いスキル層に求人を提供しています。専門学校、プログラミングスクールなどを卒業したての駆け出しエンジニアから、バリバリ開発が出来る高レベル層のエンジニアまで幅広い種類の案件がたくさんあります。

傾向

上位はJava、C、PHP, C#, JavaScriptです。市場規模を考えると業務系はWeb系の8倍弱なのでJava,C,C#が上位に来るのは当然です。PHP,JavaScriptが健闘しています。

第2章 我が国における IT 関連産業及び IT 人材の動向
https://www.meti.go.jp/policy/it_policy/jinzai/27FY/ITjinzai_report_2.pdf

C言語はJavaに比べ高年収求人が比較的少ない事が分かります。逆にRuby,PythonはC言語と比較するとそれぞれ案件数に比べて高年収求人は比較的多い事分かります。C, C#などは低年収求人が比較的多いようです。組み込み寄りの言語は人あまりが発生しているのかもしれません。

高年収求人はTypeScript, Kotlin, Scalaが高年収求人に偏っています。TypeScriptはJavaScriptの、KotlinとScalaはJavaの後方互換言語で、これから言語学習を始める方はJavaかJavaScriptをやっておけば、高年収の道は確保されていると見て良さそうです。

年収別ランキング(相対数順)

スクリーンショット 2019-10-21 17.02.18.png

単純な求人数だけの比較をしてしまうとどうしても母数の多い言語が有利になってしまうため「言語別の高年収求人の割合」を出して並び替えました。「人気がありかつ人手不足の求人ほど給料が高年収の求人割合が多い」と考えるなら、こちらのほうが人気度の実態を表していると考えられます。

傾向

400万円台までは組み込み系かWeb系、500万円台,600万円台からモダンな言語が増えていきます。700万円台ではProcessingの人気が際立っています。Processingは主に電子アートとビジュアルデザインのための言語です。その他、高年収求人には最近出て来たモダンな言語が割合多く見られます。

2019年 総合ランキング 決定版

絶対評価、相対評価だと結局どの言語が良いのか分からないので、絶対数と相対数から弊社独自の重み付けにより総合ランキングを算出しました。ランキングの仕組みは期待値を出しているだけで単純すぎて恥ずかしいので割愛しますが、要は「高年収求人の割合が多い言語」ほど上位に来る仕組みだと思ってください。(期待値のようなものです。というか期待値です。)

10位:Kotlin

Androidアプリ開発で採用する企業が年々と増えており、Android需要に後押しされた格好です。Androidアプリ開発自体はJavaでもできるため、Kotlinでの開発は選択的でiOS開発におけるSwiftほどのインパクトはなかったと見るべきでしょう。

KotlinはJavaよりもスマートに完結に書けることを目指していて、Javaとの互換性もあり、人気が高まっている言語です。最近は「サーバサイドでもkotlinで実装しよう」という動きが目立ち始めています。

国内のサーバーサイド Kotlin 公開採用事例まとめ
https://qiita.com/ptiringo/items/dd734ab8064f94139294

9位:Scala

国内ではあまり目立っていませんが「高単価求人の多さ」が援護射撃になり上位にランクイン。開発資産としてJavaライブラリが使用可能で(Kotlinも同じ)、生産性を高めるモダンな書き方も可能です。やや古い話ですが、2009年にはTwitterがバックエンドをRubyからScalaに移行しました。

Twitter、Ruby on RailsからScalaへ
https://it.srad.jp/story/09/04/10/0421223/

8位:JavaScript

SPAの需要拡大に伴いTypeScriptと共に上位に浮上しました。Adobe AcrobatがJavaScriptのマクロ機能を積んでいるなど、サードパーティ製ツールもJavaScript解析エンジンを積んでいる例が散見されます。また昨今ではJavaScriptの言語的特性(NonBlocking I/Oと相性が良い)からサーバサイドでもJavaScript(NodeJS)を使う動きが見られています。

githubでは注目度断トツの1位。世界的にも現在、もっとも注目を浴びている言語の1つとして過言ではないでしょう。
https://githut.info/

7位:Python

Pythonはシンプルで少ないコードで書けるので、C言語などと比較し扱いやすい人気言語です。近年よく耳にするAI/機械学習/統計解析に必要なライブラリを揃えている事で上位にランクイン。AI需要もありこれからもPython需要は高まっていくかもしれません。

既にAI分野のディファクトスタンダードのような扱いで、Jupyter Notebook(https://jupyter.org/ )など使えば比較的簡単に環境が手に入ります。これから学ぼうという方にも良いかもしれません。

6位:Ruby

日本で開発されたプログラミング言語で、初めて国際電気標準会議(IEC)で国際規格に認証されたことでも有名です。『Y combinator』出身の時価総額上位10社のうち6社が採用している言語で、世界のテクノロジーを支えていると言って過言ではないでしょう。Webアプリケーション開発と非常に相性がよく、日本では下火になったと言われて久しいですが、求人ベースだと人気は健在。国内のスタートアップが積極的に採用している言語の1つです。

ref
https://news.ycombinator.com/item?id=21138422

5位:TypeScript

TypeScriptはマイクロソフトによって開発されたプログラミング言語。JavaScriptに「型」の概念を持ち込みました。ここから『ReactJS』『VueJS』が生まれたと言って過言ではないでしょう。スパゲティになりがちなフロントエンド開発にオブジェクト指向の概念を持ち込み、優れた保守性を持ったSPAアプリケーション開発を実現します。

4位:Apex

SalseForceのプログラミング言語です。ApexはJavaに似ており、Java言語ユーザには親しみのある記述方法を提供しています。Salesforceは開発者に高いインセンティブを支払うことで有名であり、中小ベンダーにとって採用の敷居が低い人気言語となっています。

実は現在、人材市場ではApex開発者の争奪戦が繰り広げられており、歴3年もあればヘッドハントは当たり前。開発者からすると東京で1000万、大阪でも800万も狙える非常に魅力的なプログラミング言語となっています。

3位:PowerShell

PowerShellは2006年に生まれた言語でMicrosoft発のプログラミング言語です。WindowsやMicrosoft製品のシステム管理を行うためのシェル言語を提供しており、オブジェクト指向で開発ができることでも有名です。Bashで書くかPowerShellで書くかで悩んだ開発者も多いでしょう。現在はオープンソース化されています。

Apexと同様、この2つの言語は使用用途が偏っているため求人数が多くないのですが、その分開発経験者が少なく、高単価になったと考えられます。わざわざこれから始めようという言語ではないかもしれませんが、既に業務で経験されていたり触る機会のある方にはGood Newsかもしれませんね。

2位:Swift

Swiftは、モダンな記述で開発がしたい開発者とiOSアプリの開発需要のダブルアタックで上昇したプログラミング言語です。Swift自体は初心者にも優しく、駆け出しの方にもオススメですが、ある程度ターミナルの知識を求められるので、知識が全くないと環境構築の段階で沼にハマってしまうかもしれません。そして言うまでもないかもしれませんが、Swiftでの開発には「MacBook」が必要です。

1位:Golang

GolangはGoogleによって設計されたプログラミング言語です。動作は軽量でソフトウェアを効率的にシンプルに構築できるとされていてオススメです。Goのツール、コンパイラ、ソースコードは全てオープンソースです。実装はオブジェクト指向にも関数型にも適応しており、優れたメモリ管理アルゴリズムが非常に軽快で高いパフォーマンスを発揮します。ある程度の言語経験者には非常に人気の高いプログラミング言語ではあります。一方で低年収求人には少なく、初心者向きではないので注意が必要です。

まとめ

プログラミング言語別総合ランキングはGolangが一位を獲得しました。今までGolangには興味あるけどなんとなく遠ざけてきた開発者の方は一度Tryしてみる価値はあるかもしれません。しかし入門者が始めるには敷居が高く、これからプログラミング言語を始める人はJava/Ruby/JavaScriptあたりが良いかもしれません。全体的にモダンな言語や用途が限定的ではあるが需要の高い言語が上位に来ており、既に得意になっている言語の延長で取り組めば効率よく年収アップが狙えるかと思います。

プロモーション

『リッターズ』のTwitterアカウントでは世界中の「先端Techビジネス」や技術要素の格付け情報を流しています。気になる方は是非フォローしてみてください。

リッターズ
https://twitter.com/ritters2u

『渋谷コード塾』では求人市場の調査結果から最新の技術トレンドを調査取得し、それらを習得するための半年間のコースを提供しています。直近ではReactNativeによるネイティブアプリ開発とRuby/Python/NodeJS/Golangによるサーバサイド開発を半年でマスターする「アプリxAPI開発コース」を提供しています。「精神と時の部屋」で半年間で3年分の成果を出しましょう。HPはまだ用意していないため、興味ある方は直接私のTwitterアカウントにご連絡ください。

Twitter
https://twitter.com/shiraponsu

フォローやお仕事のお問い合わせもお待ちしています。
ここまで読んでいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

js/tsでimg要素(HTMLImageElement)をloadしたらPromiseを返す拡張メソッド(モンキーパッチ)を実装する

TL;DR

monkey-patch.ts
interface HTMLImageElement {
  asyncLoad(): Promise<void>
}

// jsなら↓だけでおk
HTMLImageElement.prototype.asyncLoad = function() {
  if (this.complete) return Promise.resolve()

  return new Promise(resolve => {
    this.addEventListener("load", function callback() {
      resolve(this)
      this.removeEventListener("load", callback)
    })
  })
}

if (this.complete) return Promise.resolve()を記述しないと、読み込み済みの場合 Promise が解決されないので注意

概要

全ての img 要素が読み込み終わったら特定のアクションを実行するとか、new Image()したやつを読み込み終わるまで await したりなどでわざわざ関数書くのも面倒なんで、モンキーパッチと呼ばれるテクニックで HTMLImageElement に asyncLoad というメソッドを追加してみました。

Effective JavaScriptにやみくもなモンキーパッチは避けるべきと書いてあるので、業務で使用する場合は上の確認を取ってから実装するように!

使用例

import "./monkey-patch.ts"

/** img要素の全てが読み込まれた場合Promiseが解決される*/
const asyncLoadAllImages = async () => {
  const promises = Array.from(document.querySelectorAll("img"), el => el.asyncLoad())
  await Promise.all(promises)
}

/** 特定パスから画像を読み込まれた場合Promise<HTMLImageElement>が解決される */
const asyncGetImage = async (src: string) => {
  const image = new Image()
  image.src = src
  await image.asyncLoad()
  return image
}

余談

Promise を返却するような非同期関数の命名規則ってどうなっているんだろう。。

基本的に関数名先頭に async を付けてるけど正直コレジャナイ感がすごい。。

あと、「Promise が解決される」って言い方はMDNにそって言ってるけど、これも日本語的にすごく違和感...?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

n-queens

めもめも

(number=>{

const init=width=>{

  const h=i=>~~(i/width), v=i=>i%width, d=i=>h(i)+v(i), u=i=>h(i)-v(i);

  const test=(grid,i,f)=>grid.filter((e,j)=>f(i)==f(j)).reduce((a,c)=>a+c,0) < 2;

  let count=0;

  const log=grid=>[...Array(width).keys()].map(e=>e*width).forEach(
    e=>((e==0)?console.log((++count)+":"+"-".repeat(width)):false)
     ||console.log(grid.slice(e,e+width))
  );

  return solve=(grid=[...Array(width**2)].fill(0),row=0)=>
    [...Array(width)].map((_,i)=>i+row*width).forEach(i=>[
      grid[i]=1,
      (test(grid,i,v)&&test(grid,i,d)&&test(grid,i,u))?
        (row+1==width?log(grid):solve(grid, row+1)):0,
      grid[i]=0
    ]);
};

init(number)();

})(8)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

備忘録:Railsにおける多次元配列のリファクタリングについて(+ jsonでの受渡しについて)

前提

json形式で配列を送りたかったが、配列が別々に2つ存在するため多次元配列(2次元配列)を作る必要があった。

最初

name.rb
names = ["hideki", "takahiro", "miki"]
descriptions = ["すごい", "かっこいい", "かわいい"]

inventories = []

names.each_with_index do |name, i|
  inventories.push [name, descriptions[i]]
end

出力結果

 [["hideki", "すごい"], ["takahiro", "かっこいい"], ["miki", "かわいい"]]

改善

name.rb
names = ["hideki", "takahiro", "miki"]
descriptions = ["すごい", "かっこいい", "かわいい"]

inventories = names.zip(descriptions)

出力結果

 [["hideki", "すごい"], ["takahiro", "かっこいい"], ["miki", "かわいい"]]

メモ:受け取り側での処理

上記をjson形式で送る

name.rb
render json: inventories

ループさせる

name.coffee
success: (json) ->
  html = ""
  for i of json
    html += "<div class='name'>#{json[i][0]}</div><div class='description'>#{json[i][1]}</div>"
  $(".names").html(html)

上記の[0][1]をループさせる方法がわからずでして、、どなたかわかる方がいらっしゃいましたら教えて頂けるとうれしみです。。

参考にした記事

Rubyの配列でごにょごにょするときzipとinjectとevalが便利すぎる件

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

9行のhtmlでスネークゲームを作った(Qiitaで遊べるよ!!)

はじめに

200行のVue.jsでスネークゲームを作った」、「たった7行でテトリスを実装「七行プログラミング」とは」の影響を受けて「23行のhtmlでマインスイーパーを作った(Qiitaで遊べるよ!!)」という記事を書いたのだが、トレンド入りしたり影響を受けた記事が作られたりするなど個人的には反響が大きかったように思える。
そこで今回はショートコーディング第二弾として、スネークゲームを可能な限り小さいhtmlで制作した。
最終的に行数はたった1桁の9行、サイズは1KBを切るスネークゲームの完成に成功した。

さあ、スネークゲームで遊ぶのだ!

See the Pen HTML_Snake_Min by T.D (@td12734) on CodePen.

大きい画面で遊びたい人はこちら
Qiitaだと上下移動でスクロールされてしまうので大画面推奨です。

ゲームルール

  • ヘビ(緑色)を動かしてエサ(赤色)を取得するゲームです。
  • 矢印キーを押すとヘビの体を操作できます。
    • 一度上の枠内をクリックしてからでないと矢印キーが反応しないかもしれません。
    • 最初に矢印キーを押した後、ヘビが動き始めます。
    • ヘビは止まらずに動き続けます。
  • エサを取るとヘビの体が伸びます。
  • ヘビの頭が胴体に衝突する、盤外に行くとゲームオーバーです。
  • Resetボタンを押せばゲームをやり直せます。

プログラム

コード

ゲームは以下のコードで実装した。七行プログラミングのルールに従い、1行は79文字以下としている。

<!doctype html><body onKeyDown=K=event.keyCode-36 onload='L=f=>{i=t;while(i--)f
(C(i))};C=c=>T.rows[c/w|0].cells[c%w];D=_=>L(c=>c.style.background=!c.v?0:!~c.v
?"red":c.v-s?"tan":"lime");F=_=>(c=C(M())).v?F():c.v--;M=_=>Math.random()*t|0;R
=_=>{L(c=>c.v=P[I="innerHTML"]=0);C(p=M()).v=s=1;D(F(p++))};for(i=K=0;i<(t=400)
;i++){i%(w=20)?0:r=T.insertRow(0);r.insertCell(0).style="width:24px;height:8px"
+";border:solid"}R(setInterval(_=>{if(+P[I]>=P[I]&&K){if((K>3&&(p+=w)?p<=t:K>2
&&p++?p%w-1:K>1&&(p-=w)?p>0:p--&&p%w)&&C(p-1).v<1){C(p-1).v&&!F(P[I]=s++)?0:L(
c=>c.v>0&&c.v--);D(C(p-1).v=s)}else P[I]+=" Gameover"}},200))'><input onclick=
R() type=button value=Reset><p id=P><table id=T style=border-collapse:collapse>

htmlで保存してブラウザで開いても恐らく遊べます。
最終行を始めとするほぼ全ての行が79文字埋まっており、視覚的にも大変美しいと思います。
主な実装はbodyのonloadに書いてあるが、何書いてあるか分からないと思うのでonloadを書き下したコードを以下に貼る。

onload
L=f=>{i=t;while(i--)f(C(i))};
C=c=>T.rows[c/w|0].cells[c%w];
D=_=>L(c=>c.style.background=!c.v?0:!~c.v?"red":c.v-s?"tan":"lime");
F=_=>(c=C(M())).v?F():c.v--;
M=_=>Math.random()*t|0;
R=_=>{
  L(c=>c.v=P[I="innerHTML"]=0);
  C(p=M()).v=s=1;
  D(F(p++))
};
for(i=K=0;i<(t=400);i++){
  i%(w=20)?0:r=T.insertRow(0);
  r.insertCell(0).style="width:24px;height:8px;border:solid"
}
R(setInterval(_=>{
  if(+P[I]>=P[I]&&K){
    if((K>3&&(p+=w)?p<=t:K>2&&p++?p%w-1:K>1&&(p-=w)?p>0:p--&&p%w)&&C(p-1).v<1){
      C(p-1).v&&!F(P[I]=s++)?0:L(c=>c.v>0&&c.v--);
      D(C(p-1).v=s)
    }
    else P[I]+=" Gameover"
  }
},200))

改行しても23行しかない。
ただ、見やすくなったもののコードの解読は困難だと思うので以下で説明を行う。

変数、関数の説明

コード圧縮の都合上、全ての変数名と関数名は1文字になっている。
その結果、可読性が大幅に失われたのでここでは変数名、関数名について説明する。

  • 変数、関数名
    • 1文字に圧縮する前に付けたであろう名前
    • この行以降は動作説明など

グローバル変数

  • i
    • 特に無し
    • ループ調整変数
  • p
    • player
    • ヘビの頭の座標+1
  • r
    • tableRow
    • テーブルの行
  • t
    • tableSize
    • テーブルのマスの数、及びsetTimeoutのインターバル
  • w
    • width
    • テーブルの横サイズ
  • I
    • innerHTML
    • 定数"innerHTML"
  • K
    • keyCode
    • 入力したkeyCodeから36を引いたもの
  • P
    • pElement
    • スコアとGameOverを表示するテキストエリア
  • T
    • tableElement
    • ゲームの盤面

ローカル変数

  • c
    • cell
    • テーブルのセル
  • f
    • function
    • Lで実行する関数
  • v
    • cellValue
    • そのセルの数値
    • 0:何もない
    • 1以上:ヘビの体
    • -1:エサ
  • _
    • 特に無し
    • アロー関数の()を省略するために定義したダミー変数

関数

  • C
    • GetCell
    • テーブルのセルを取得する
  • D
    • DrawCell
    • ゲームの盤面のマスに色を塗る
  • F
    • FoodSet
    • 盤面にエサを設置する
  • L
    • LoopCell
    • 全てのセルに対して引数の関数を実行する
  • M
    • Math_random
    • エサとヘビの位置をランダムに決める
  • R
    • ResetGame
    • ゲームの盤面を初期化する

動作、工夫点の説明

説明を見る前に最低限知っておきたい知識

  • 変数宣言のvarletは省略可能
  • 変数宣言や計算などは文中でも可能
    • 場所によってはカッコで括る必要がある
  • if-else文より三項演算子を使った方が短くなる場合がほとんど
    • if(条件) 処理1 else 処理2 → 条件?処理1:処理2
  • 0はfalse、0以外はtrueと判断される
    • -1をビット反転させれば0になる(~-1=0)
  • 関数宣言はfunctionよりもアロー関数を使った方が短い
    • function func(a){}func=a=>{}
    • function (){}_=>{}(「_」はダミー変数)
  • if,while,アロー関数などの{}の中の処理が1つの時、{}を省略可能

html部分

<!doctype html>
<body onKeyDown=K=event.keyCode-36 onload='省略'>
<input onclick=R() type=button value=Reset>
<p id=P>
<table id=T style=border-collapse:collapse>

html5ではp,tableなどの閉じタグが無くても動作するのでこれらを削除する。
ただ、環境によってはhtml5と認識されずにエラーが出るので<!doctype html>を書く必要がある。

キーボードのキーが押された時、bodyのonKeyDownが呼ばれて変数Kにキーコード-36が代入される。
(←,↑,→,↓)のキーコードは(37,38,39,40)なので、あらかじめ36を引けばKには(1,2,3,4)が代入されてKの比較時などに文字数を削減可能。

JavaScriptはscriptタグではなくbodyのonloadに全て突っ込む。
こうすれば<script></script>の文字列を省略可能で、全てがload時に実行されるのでwindow.onloadも省略可能で文字数を大幅に削減できる。
ちなみに</script>は省略できない。

inputタグではResetボタンを定義し、押した時に関数Rを呼び出すようにしている。

pタグでは点数とGameoverの文字を表示する。
idをPにし、他にP変数や関数を定義しなければgetElementById()をしなくてもP.~の形でpタグにアクセス可能。
同様の事をtableでもやっている。

ゲームの盤面はtableタグで表示している。
styleのborder-collapsecollapseにしなければ枠線が表示されず、ゲームの難易度が跳ね上がる。

ゲームの盤面を設定する

for(i=K=0;i<(t=400);i++){
  i%(w=20)?0:r=T.insertRow(0);
  r.insertCell(0).style="width:24px;height:8px;border:solid"
}

真ん中よりやや下にある部分だがここが最初に実行される。
1行目はfor文で0~399まで回し、ついでにKに0、tに400を代入している。
2行目ではiを20で割った余りが0のとき、insertRowして行を追加している。
本来は以下に示すif文だが、三項演算子を用いる事で文字を削減できる。
また0がfalse扱いされることを利用して!==0も省略している。

if(!(i%(w=20)!==0))r=T.insertRow(0);

3行目ではテーブルのセルのスタイルを設定している。
セルを横長な長方形にし、境界線を1本だけ引くようにして盤面を見やすくしている。

ゲームテーブルの特定のセルを取得する

C=c=>T.rows[c/w|0].cells[c%w];

座標が引数cであるセルを返している。
分かりやすく書くと以下と同様の処理をしている。

function C(c) {
  return T.rows[Math.floor(c/w)].cells[c%w];
}

小数点以下の切り捨てはMath.floor()を使うよりもビット演算して|0する方が断然短い。

ゲームテーブルの全てのセルに対し、セルを引数とする関数を実行させる

L=f=>{i=t;while(i--)f(C(i))};

テーブルの全てのセルに対し、セルを引数とする関数fを実行させている。
全てのセルの呼び出しはforよりもwhileの方が短く書ける。

ゲームテーブルに色を塗る

D=_=>L(c=>c.style.background=!c.v?0:!~c.v?"red":c.v-s?"tan":"lime");

この1行で色塗り処理を行っている。
読みやすく書き直すと以下のようになる。

function D() {
  L(function (c) {
    if (c.v===0) {
      c.style.background="white";
    }
    else if (c.v===-1) {
      c.style.background="red";
    }
    else if (c.v-s!==0) {
      c.style.background="tan";
    }
    else {
      c.style.background="lime";
    }
  });
}

if文は三項演算子で簡略化し、条件式は0がfalse、0以外がtrue、-1のビット反転が0でfalseである事を利用して文字数を減らしている。
色について、whiteは0で代用可能なので0を代入している。
エサは通常は赤色で示し、redは3文字しかないのでredにした。
ヘビの頭は通常は緑色で示されるが、greenは5文字で長いので4文字のlimeで代用した。
ヘビの胴体は3文字の色であるtanにした。

プレイヤーやエサを配置するセルの座標をランダムに決める

M=_=>Math.random()*t|0;

ランダムな座標、つまり0から399までのランダムな整数を返している。
切り捨てはビット演算で行っている。

何も無いランダムな座標にエサを配置する

F=_=>(c=C(M())).v?F():c.v--;

分かりやすく書くとこうなる。

function F() {
  c = C(M());
  if (c.v!==0) {
    F();
  } else {
    c.v--;
  }
}

まず、セルcをランダム決めている。
次にセルcの値が0ではない、つまりエサかヘビの頭か胴体ならFを再度呼び出し、0ならセルの値から1を引いて-1にしてセルをエサ扱いする。

ゲームを初期化する

R=_=>{
  L(c=>c.v=P[I="innerHTML"]=0);
  C(p=M()).v=s=1;
  D(F(p++))
};

関数Rで全てのセルのリセット、スコアのリセット、プレイヤーのランダム配置、エサのランダム配置を行っている。
分かりやすく書くと以下のようになる。

function R() {
  L(function (c) {
    c.v=0;
    P[I="innerHTML"]=0
  });
  C(p=M()).v=s=1;
  p++;
  F();
  D();
}

最後のD(F(p++))について、関数FとDは引数を参照しないのでどのような引数を入れても処理に支障はない。
そこでFの引数でp++を行い、Dの引数で関数F(p++)を呼び出してセミコロンを減らしている。

200ミリ秒ごとに行う処理を記述し、ゲームの初期化を行う

R(setInterval(_=>{
  if(+P[I]>=P[I]&&K){
    if((K>3&&(p+=w)?p<=t:K>2&&p++?p%w-1:K>1&&(p-=w)?p>0:p--&&p%w)&&C(p-1).v<1){
      C(p-1).v&&!F(P[I]=s++)?0:L(c=>c.v>0&&c.v--);
      D(C(p-1).v=s)
    }
    else P[I]+=" Gameover"
  }
},200))

恐らくこのソースコードで最も難解な場所である。
分かりやすく書くと以下のようになる。

setInterval(function (){
  if(Number(P[I])>=P[I]&&K!==0){ //pタグ内の文字列を数字に変換可能で、Kが0ではない時
    var notSnakeCollided; //ヘビが衝突していなければtrue、説明用変数
    if (K>3) { //下に移動
      p+=w;
      notSnakeCollided=p<=t;
    }
    else if (K>2) { //右に移動
      p++;
      notSnakeCollided=p%w-1;
    }
    else if (K>1) { //上に移動
      p-=w;
      notSnakeCollided=p>0;
    }
    else { //左に移動
      p--;
      notSnakeCollided=p%w;
    }
    if (notSnakeCollided&&C(p-1).v<1) { //ヘビが壁に衝突しておらず、移動先がヘビの胴体ではない時
      if (C(p-1).v!==0) { //セルの値が0ではない、つまりエサの時
        s++;
        P[I]=s;
        F();
      } else {
        L(function (c) {
          if (c.v>0) {
            c.v--;
          }
        });
      }
      C(p-1).v=s;
      D();
    } else { //敗北条件を満たした時はpタグの文字列にGameoverを追加する
      P[I]+=" Gameover";
    }
  }
},200);
R();

setIntervalでは200ミリ秒ごとに以下の処理をやらせている。

  1. ゲームオーバーでは無く、ゲームがスタートしているなら次に進む
  2. Kの値によってヘビを移動させ、壁やヘビに衝突していないか確認する
  3. 衝突していないとき、以下の処理を行う
    1. 移動先がエサのとき、スコアを1増やしてエサを再配置する。
    2. 移動先がエサではないとき、ヘビのセルの値を1減らす。
    3. 移動先の座標をヘビの頭にする
    4. テーブルに現在の状態を反映させる
  4. 衝突していたとき、pタグの文字列にGameoverを追加する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptで録音サイトを作ってみたVer.2

Recorder.jsで録音サイトを作りました。
名前は「Recood」です。
Screenshot from 2019-10-21 17-01-58.png
基本的には、真ん中のマイクのボタンを押すと録音を開始することができると思います。

対応ブラウザ

Google Chrome(iOS、iPad OS版はわからないのでできればコメントでお願いします。)
URL https://anyfre.github.io/recood/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsで学ぶ、Vue.jsコンポーネント設計の基本

はじめに

Nuxt.js(以下Nuxt)は、Vue.js(以下Vue)をサーバーサイドで動かす目的以外にも、ディレクトリ構造やVueエコシステムのライブラリがセットになっているため、設計の工数を削減する目的で採用するケースもあると思います!

お中元に迷ったときのヨックモックみたいですね?

本記事では、Nuxtのディレクトリ構造を土台とし、Vueのコンポーネント設計について、各レイヤー(ディレクトリ)のコンポーネントが担うべき責務をまとめました。

Vue3.0がやってくると、Composition API 導入に伴い設計のベストプラクティスが変化すると思いますが、Vue2系を触ってきた個人的な総括の気持ちで書いています。
※SSR(BFF)とライフサイクルをテーマに、もう一つ記事を書くつもりでいます。

Vueコンポーネントの基本とNuxtでの例

Vueコンポーネントは下記図のように、ツリー状にネストし構築されていきます。
components.png
※Vue.js公式サイト コンポーネントによる構成より

DOMもツリーなので、Webエンジニアには馴染みやすい概念ですよね。

Nuxtでは下記図のように、Layout > Page > Optional Component というツリー構造でVueコンポーネントがネストされていきます。
※参考: Nuxt公式サイトのビュー概要図

Vueコンポーネントの責務について

NuxtではasyncDataメソッドをVueコンポーネントに実装可能ですが、このメソッドはPageコンポーネント以外で利用することはできません

Nuxtを使った人は、一度はasyncDataメソッドをPage以外の場所で使おうとしたのではないかと思いますが、なぜこのような作りになっているのでしょう。

asyncDataのようにコンポーネント毎(ツリーの階層ごと)で可能な処理が異なる = 責務が明確化されている ことで、外部のAPIやStore(VueのデファクトだとVuex)に依存している箇所が明確になる、テスタビリティが向上する、メンテしやすいCSS設計へ貢献するといった効果を見込めます。

そんなNuxtですが、Componentsディレクトリ以下ではコンポーネント設計の指針を設定していません。

すべてのロジックをPageとStoreに詰め込むと、Storeとのやり取りを行うPageコンポーネントが肥大化しやすいため、次章ではStoreのやり取りをComponents以下に持たせる設計の定番を紹介します。

Presentational and Container パターン

Reactで提唱されたこちらの名作記事とともに、モダンフロントエンドのコンポーネント分割における最も有名なパターンだと思います。

Redux(Fluxを基にした状態管理ライブラリ)公式サイトでは次のように言及されています。

Presentational Container
見た目に関すること 動作に関すること
Storeとの疎通禁止 Storeと疎通する

VueではStoreとしてVuexを採用するケースが多い(Nuxtでは標準)と思いますが、コンポーネント分割はReactなどと同様のルールが採用可能です。

コンポーネントをネストする規則

原典ではコンポーネントをネストする際のルールはかなり自由です。
Containerの中にContainerもPresentationalも入れてOKですし、反対にPresentationalの中にContainerもPresentationalも入れてOKとされています。
しかし実際の運用では、PresentationalはContainerを読み込めない、といったルールを設けることをオススメします。

アメブロでの実装例では、Atomic DesignのOrganismsをContainerとし、MoleculesとAtomsをPresentationalとして設定することで役割をより明確化しています。

※ Atomic Design について
Atomic Design の運用は、通常フロントエンドエンジニアだけでは実現できません。デザイナーの協力が不可欠で、チームごとに適用すべきかどうか異なると思います。

Nuxt設計例

以上の話をふまえ、Nuxtアプリケーションの設計の具体例を紹介します。

ディレクトリ構成

├── components
│   ├── container // pageごとにcontainerを管理する。このレイヤーではComponentの使いまわしを意識しない
│   │   ├── page1
│   │   ├── page2
│   │   └── shared // Containerを複数のページでimportする場合に用いる
│   └── presentational // Atomic designでの一例。他にはbuttonなどのパーツでディレクトリ切るのも?‍♀️
│       ├── molecules
│       └── atoms
├── pages
│   ├── page1
│   └── page2
│       ├── edit.vue
│       └── index.vue
├── layouts
│   ├── default.vue
│   └── error.vue
└── store
    ├── store1.ts
    └── index.ts

役割早見表

Storeとの疎通 外部APIとの通信 テストコード Style Props/Slot import可能
Container × なるべく書かない Presentational配置のみ Pageに依存する場合のみ Store, Presentational
Presentational × × なるべく書かない (molecules)Presentational, (atoms)×
Page なるべく書かない ContainerとPresentational配置のみ × Store, Container, Presentational
Layout × × × PageとContainerとPresentational配置のみ × Page, Container, Presentational
Store - - - ×

※Componentのテストは難しいので(特にメンテナンス…)、アプリケーションにとって重要なロジックは、テスタビリティを保ちやすいStoreに寄せていくのを強くオススメします。

StateをPageに持たせるかStoreに持たせるか

NuxtはStoreのAction以外でも、asyncDataのようにComponentから外部APIと通信することを想定されたメソッドが存在しています。

Vue.jsはComponent内にローカルStateを持つことが簡単で、Vuexを用いたFluxパターンとv-modelに代表される双方向データバインディングを共存させることが出来ます。

個人的にはどちらを選ぶかというよりも、アプリケーションにとって重要なStateであるならばStoreに持たせて、そうでないならComponentのローカルStateとして取り扱うといった使い分けが良いと考えています。

※テスタビリティまで考えるとすべてのロジックをStoreに寄せていった方がメンテしやすいです。一方そのようにしてFluxに準拠するならば、Reactを採用すべきでVueを採用するメリットが薄くなります。
TypeScriptとの相性であったりフロントエンドのテスタビリティを含め、より安全なアプリケーションが求められる環境では、Reactの方が適正が高い(結果として工数が少なくなる)と最近は感じます。そのためVue3.0は期待大?‍♀️

おまけ: サンプルコード

コードを読んだ方が理解しやすい方向け。
7ケタの郵便番号を入力すると、次のページへ進めるボタンが押せるページの部分的なサンプルコードを記載しておきます。

ポイント
・StoreにcommitしてるのはContainerのみ。Presentationalは必ずPropsで受け取る。
・PageはStoreと疎通しているがgetterで参照してるだけ。この設計はテストしやすくオススメ。
・型定義などは@typesに記述するか、Storeに寄せていきそこからimportして使うことをオススメ。
・今回解説を省略してますが、小さなatomsはSassのmixinsを用いて作るとコードが少なくなり楽です。

~/pages/inquiry/edit.vue
<template>
  <main class="edit">
    <TitleIcon class="titleIcon" title="お問い合わせ" icon="inquiry" />
    <UserInput class="buttonLink" />
    <ButtonLink
      class="buttonLink"
      link="/inquiry/confirm"
      text="確認へ進む"
      :disabled="$store.getters['user/isCompleteInput']"
    />
  </main>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

import UserInput from '~/components/container/inquiry/UserInput.vue'
import TitleIcon from '~/components/presentational/atoms/TitleIcon.vue'
import ButtonLink from '~/components/presentational/atoms/ButtonLink.vue'

@Component({
  components: {
    UserInput, TitleIcon, ButtonLink
  }
})
export default class InquiryEdit extends Vue {}
</script>

<style lang="scss" scoped>
.titleIcon {
  margin-bottom: 40px;

  @include isPc() {
    margin-bottom: 60px;
  }
}

.buttonLink {
  margin-bottom: 80px;

  @include isPc() {
    margin-bottom: 120px;
  }
}
</style>
~/components/container/inquiry/UserInput.vue
<template>
  <div>
    <h2 class="heading">郵便番号</h2>
    <ValidateNumberInput
      :value="postalCode"
      :digit="7"
      placeholder-text="7ケタの郵便番号を入力してください"
      @input="inputCode"
    />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

import ValidateNumberInput from '~/components/presentational/molecules/ValidateNumberInput.vue'

// https://github.com/championswimmer/vuex-module-decorators#accessing-modules-with-nuxtjs
import { userStore } from '~/store'

@Component({
  components: {
    ValidateNumberInput
  }
})
export default class UserInput extends Vue {
  public postalCode: string = userStore.postalCode

  public inputCode(v: string, err: string): void {
    this.postalCode = v
    if (err) {
      userStore.setPostalCode('')
      return
    }

    userStore.setPostalCode(v)
  }
}
</script>

<style lang="scss" scoped>
.heading {
  @include heading()
}
</style>
~/components/presentational/molecules/ValidateNumberInput.vue
<template>
  <div>
    <NumberInput
      v-model="number"
      class="numberInput"
      :icon="icon"
      :is-error="error.length > 0"
      :placeholder-text="placeholder"
    />
    <ErrorMessage :text="error" />
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'nuxt-property-decorator'

import NumberInput from '~/components/presentational/atoms/NumberInput.vue'
import ErrorMessage from '~/components/presentational/atoms/ErrorMessage.vue'

@Component({
  components: {
    NumberInput,
    ErrorMessage
  }
})
export default class ValidateNumberInput extends Vue {
  @Prop() digit!: number
  @Prop() value!: string
  @Prop() placeholder!: string

  public error: string = ''

  private validateNumber(v: string | null): boolean {
    if (v === null) return false

    const regexp = new RegExp(`^[0-9]{${this.digit}}$`)
    return regexp.test(v)
  }

  get number(): string {
    return this.value
  }

  set number(v: string): void {
    this.error = ''
    if (!this.validateNumber(v)) {
      this.error = this.errorMessage
    }
    this.$emit('input', v, this.error)
  }

  get errorMessage(): string {
    return `${this.digit}桁の数字を入力してください。`
  }

  get icon(): string | null {
    if (!this.number) return null
    if (this.error !== '') {
      return 'errorIcon'
    }

    return 'okIcon'
  }
}
</script>

<style lang="scss" scoped>
.numberInput {
  margin-bottom: 8px;
}
</style>
~/components/presentational/atoms/ErrorMessage.vue
<template>
  <transition name="fade">
    <p v-show="text" class="errorMessage">{{ text }}</p>
  </transition>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'nuxt-property-decorator'

@Component
export default class ErrorMessage extends Vue {
  @Prop() text!: string
}
</script>

<style lang="scss" scoped>
.errorMessage {
  color: $error;
}

.fade-enter-active {
  transition: opacity 0.3s;
}

.fade-enter {
  opacity: 0;
}
</style>
~/store/user.ts
import { Module, VuexModule, Mutation } from 'vuex-module-decorators'

export interface User {
  postalCode: string
}

@Module({ stateFactory: true, name: 'user', namespaced: true })
export default class extends VuexModule {
  public postalCode: User['postalCode'] = ''

  @Mutation
  public setPostalCode(code: string): void {
    this.postalCode = code
  }

  public get isCompleteInput(): boolean {
    return this.postalCode !== ''
  }
}

※動作確認してないので、間違った部分あったらごめんなさい?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ピュアJSでかな入力の薙刀式練習タイピングソフトを作った

かな入力の薙刀式を練習するために、JavaScriptでタイピングソフトを作ってみました。

薙刀式とは

かな入力の配列の一つです。よく出る表現を楽に打つことを目的とした配列です。
薙刀式を考案された大岡さんのページはこちらです。
http://oookaworks.seesaa.net/article/456099128.html

かな入力配列はほかにもありますので、良かったら調べてみてください。(タイピング速度最速は新下駄配列だといわれています。)

作成したタイピングソフト

https://akrolayer.github.io/Portfolio/NaginataTyping.html

QWERTY配列のままタイピングすると薙刀式のかなが入力されるようにしています。実際に配列を変えるのは設定変更が面倒くさく、ハードルになると思うので、このソフトで体験してもらいたいと考えて作りました。実際に良いと思い、慣れてきたら実際に配列変更していただくとよいと思います。

このサイトのタイピングゲームの基本機能部分を参考にさせていただきました。ありがとうございます。
https://www.pazru.net/js/key/3.html

コードはこちらです。

薙刀式の押下されたキーの組み合わせで入力される文字を返す関数
function reaction(keyStatus){ //入力された文字の判定 
    if(keyStatus[32]){//シフト同時押し
        if(keyStatus[70]){//シフトと左手濁音キー(f)押し
            if(keyStatus[81]) return "";//q
            if(keyStatus[87]) return "";//w
            if(keyStatus[69]) return "";//e
            if(keyStatus[82]) return "";//r
            if(keyStatus[85]) return "";//u
            if(keyStatus[73]) return "";//i
            if(keyStatus[79]) return "";//o
            if(keyStatus[80]) return "";//p
            if(keyStatus[65]) return "";//a
            if(keyStatus[83]) return "";//s
            if(keyStatus[68]) return "";//d
            if(keyStatus[71]) return "";//g
            if(keyStatus[72]) return "";//h
            if(keyStatus[74]) return "";//j
            if(keyStatus[75]) return "";//k
            if(keyStatus[76]) return "";//l
            if(keyStatus[186]) return "";//;
            if(keyStatus[90]) return "";//z
            if(keyStatus[88]) return "";//x
            if(keyStatus[67]) return "";//c
            if(keyStatus[86]) return "";//v
            if(keyStatus[66]) return "";//b
            if(keyStatus[78]) return "";//n
            if(keyStatus[77]) return "";//m
            if(keyStatus[188]) return "";//,
            if(keyStatus[190]) return "";//.
            if(keyStatus[191]) return "";///
            return "";//f
        }
        if(keyStatus[74]){//シフトと右手濁音キー(j)押し
            if(keyStatus[81]) return "";//q
            if(keyStatus[87]) return "";//w
            if(keyStatus[69]) {
                if(keyStatus[79]) return "でゅ";
                return "";//e
            }
            if(keyStatus[82]) {
                if(keyStatus[186]) return "じゃ";
                if(keyStatus[79]) return "じゅ";
                if(keyStatus[80]) return "じぇ";//p
                if(keyStatus[73]) return "じょ";
                return "";//r
            }
            if(keyStatus[85]) return "";//u
            if(keyStatus[73]) return "";//i
            if(keyStatus[79]) return "";//o
            if(keyStatus[80]) return "";//p
            if(keyStatus[65]) return "";//a
            if(keyStatus[83]) {
                if(keyStatus[186]) return "ぎゃ";
                if(keyStatus[79]) return "ぎゅ";
                if(keyStatus[73]) return "ぎょ";
                return "";//s
            }
            if(keyStatus[68]) return "";//d
            if(keyStatus[70]) return "";//f
            if(keyStatus[71]) {
                if(keyStatus[186]) return "ぢゃ";
                if(keyStatus[79]) return "ぢゅ";
                if(keyStatus[73]) return "ぢょ";
                if(keyStatus[80]) return "ぢぇ";//p
                return "";//g
            }
            if(keyStatus[72]) return "";//h 
            if(keyStatus[75]) return "";//k
            if(keyStatus[76]) return "";//l
            if(keyStatus[186]) return "";//;
            if(keyStatus[90]) return "";//z
            if(keyStatus[88]) {
                if(keyStatus[186]) return "びゃ";
                if(keyStatus[79]) return "びゅ";
                if(keyStatus[73]) return "びょ";
                return "";//x
            }
            if(keyStatus[67]) return "";//c
            if(keyStatus[86]) return "";//v
            if(keyStatus[66]) return "";//b
            if(keyStatus[78]) return "";//n
            if(keyStatus[77]) return "";//m
            if(keyStatus[188]) return "";//,
            if(keyStatus[190]) return "";//.
            if(keyStatus[191]) return "";///
            return "";//j
        }
        if(keyStatus[86]){//シフトと左手半濁音キー(v)押し//右手は存在しない
            if(keyStatus[81]) return "";//q
            if(keyStatus[87]) return "";//w
            if(keyStatus[69]) return "";//e
            if(keyStatus[82]) return "";//r
            if(keyStatus[85]) return "";//u
            if(keyStatus[73]) return "";//i
            if(keyStatus[79]) return "";//o
            if(keyStatus[80]) return "";//p
            if(keyStatus[65]) return "";//a
            if(keyStatus[83]) return "";//s
            if(keyStatus[68]) return "";//d
            if(keyStatus[70]) return "";//f
            if(keyStatus[71]) return "";//g
            if(keyStatus[72]) return "";//h
            if(keyStatus[74]) return "";//j
            if(keyStatus[75]) return "";//k
            if(keyStatus[76]) return "";//l
            if(keyStatus[186]) return "";//;
            if(keyStatus[90]) return "";//z
            if(keyStatus[88]) return "";//x
            if(keyStatus[67]) return "";//c 
            if(keyStatus[66]) return "";//b
            if(keyStatus[78]) return "ぉ ";//n
            if(keyStatus[77]) return "";//m
            if(keyStatus[188]) return "";//,
            if(keyStatus[190]) return "";//.
            if(keyStatus[191]) return "";///
            return "";//v
        }
        //清音
        if(keyStatus[81]) {
            if(keyStatus[80]) return "ヴぇ";
            if(keyStatus[78]) return "ヴぉ";
            if(keyStatus[79]) return "ヴゅ";
            return "";//q
        }
        if(keyStatus[87]) {
            if(keyStatus[186]) return "みゃ";
            if(keyStatus[79]) return "みゅ";
            if(keyStatus[73]) return "みょ";
            return "";//w
        }
        if(keyStatus[69]) {
            if(keyStatus[186]) return "りゃ";
            if(keyStatus[79]) return "りゅ";
            if(keyStatus[73]) return "りょ";
            return "";//e
        }
        if(keyStatus[82]) {
            if(keyStatus[186]) return "しゃ";
            if(keyStatus[80]) return "しぇ";
            if(keyStatus[79]) return "しゅ";
            if(keyStatus[73]) return "しょ";
            return "";//r
        }
        if(keyStatus[85]) return "";//u       
        if(keyStatus[83]) {
            if(keyStatus[186]) return "きゃ";
            if(keyStatus[79]) return "きゅ";
            if(keyStatus[73]) return "きょ";
            return "";//s
        }
        if(keyStatus[65]) return "";//a
        if(keyStatus[68]) {
            if(keyStatus[186]) return "にゃ";
            if(keyStatus[79]) return "にゅ";
            if(keyStatus[73]) return "にょ";
            return "";//d
        }
        if(keyStatus[70]) return "";//f
        if(keyStatus[71]) {
            if(keyStatus[186]) return "ちゃ";
            if(keyStatus[79]) return "ちゅ";
            if(keyStatus[80]) return "ちぇ"
            if(keyStatus[73]) return "ちょ";
            return "";//g
        }
        if(keyStatus[72]) return "";//h
        if(keyStatus[74]) {
            if(keyStatus[76]) return "つぁ";
            if(keyStatus[75]) return "つぃ";
            return "";//j
        }
        if(keyStatus[75]) return "";//k
        if(keyStatus[76]) {
            if(keyStatus[80]) return "つぇ";
            if(keyStatus[78]) return "つぉ";
            return "";//l
        }

        if(keyStatus[90]) return "";//z
        if(keyStatus[88]) {
            if(keyStatus[186]) return "ひゃ";
            if(keyStatus[79]) return "ひゅ";
            if(keyStatus[73]) return "ひょ";
            return "";//x
        }
        if(keyStatus[80]) return "";//p
        if(keyStatus[67]) return "";//c
        if(keyStatus[86]) return "";//v
        if(keyStatus[66]) return "";//b
        if(keyStatus[78]) return "";//n
        if(keyStatus[77]) return "";//m
        if(keyStatus[188]) return "";//,
        if(keyStatus[190]) {
            if(keyStatus[74]) return "ふぁ";
            if(keyStatus[75]) return "ふぃ";
            if(keyStatus[80]) return "ふぇ";
            if(keyStatus[78]) return "ふぉ";
            if(keyStatus[79]) return "ふゅ";
            return "";//.
        }
        if(keyStatus[191]) return "";///
        if(keyStatus[186]) return "";//;
        if(keyStatus[79]) return "";//o
        if(keyStatus[73]) return "";//i
    }
    else{//シフトなし
        if(keyStatus[74]){//右手濁音キー(j)押し
            if(keyStatus[81]) return "";//q
            if(keyStatus[87]) return "";//w
            if(keyStatus[69]) {
                if(keyStatus[75]) return "でぃ";
                return "";//e
            }
            if(keyStatus[82]) return "";//r
            if(keyStatus[73]) return "";//i
            if(keyStatus[79]) return "";//o
            if(keyStatus[80]) return "";//p
            if(keyStatus[65]) return "";//a
            if(keyStatus[83]) return "";//s
            if(keyStatus[68]) {
                if(keyStatus[76]) return "どぅ";
                return "";//d
            }
            if(keyStatus[70]) return "";//f
            if(keyStatus[71]) return "";//g
            if(keyStatus[72]) return "";//h
            if(keyStatus[75]) return "";//k
            if(keyStatus[76]) return "";//l
            if(keyStatus[186]) return "";//;
            if(keyStatus[90]) return "";//z
            if(keyStatus[88]) return "";//x
            if(keyStatus[67]) return "";//c
            if(keyStatus[86]) return "";//v
            if(keyStatus[66]) return "";//b
            if(keyStatus[78]) return "";//n
            if(keyStatus[77]) return "";//m
            if(keyStatus[188]) return "";//,
            if(keyStatus[190]) return "";//.
            if(keyStatus[191]) return "";///
            return "";//j
        }
        if(keyStatus[70]){//左手濁音キー(f)押し
            if(keyStatus[81]) return "";//q
            if(keyStatus[87]) return "";//w
            if(keyStatus[69]) return "";//e
            if(keyStatus[82]) return "";//r
            if(keyStatus[73]) return "";//i
            if(keyStatus[79]) return "";//o
            if(keyStatus[80]) return "";//p
            if(keyStatus[65]) return "";//a
            if(keyStatus[83]) return "";//s
            if(keyStatus[68]) return "";//d
            if(keyStatus[71]) return "";//g
            if(keyStatus[72]) return "";//h
            if(keyStatus[74]) return "";//j
            if(keyStatus[75]) return "";//k
            if(keyStatus[76]) return "";//l
            if(keyStatus[186]) return "";//;
            if(keyStatus[90]) return "";//z
            if(keyStatus[88]) return "";//x
            if(keyStatus[67]) return "";//c
            if(keyStatus[86]) return "";//v
            if(keyStatus[66]) return "";//b
            if(keyStatus[78]) return "";//n
            if(keyStatus[77]) return "";//m
            if(keyStatus[188]) return "";//,
            if(keyStatus[190]) return "";//.
            if(keyStatus[191]) return "";///
            return "";//f
        }
        if(keyStatus[86]){//左手半濁音キー(v)押し
            if(keyStatus[81]) return "";//q
            if(keyStatus[87]) return "";//w
            if(keyStatus[69]) return "";//e
            if(keyStatus[82]) return "";//r
            if(keyStatus[73]) return "";//i
            if(keyStatus[79]) return "";//o
            if(keyStatus[80]) return "";//p
            if(keyStatus[65]) return "";//a
            if(keyStatus[83]) return "";//s
            if(keyStatus[68]) return "";//d
            if(keyStatus[70]) return "";//f
            if(keyStatus[71]) return "";//g
            if(keyStatus[72]) return "";//h
            if(keyStatus[74]) return "";//j
            if(keyStatus[75]) return "";//k
            if(keyStatus[76]) return "";//l
            if(keyStatus[186]) return "";//;
            if(keyStatus[90]) return "";//z
            if(keyStatus[88]) return "";//x
            if(keyStatus[67]) return "";//c
            if(keyStatus[66]) return "";//b
            if(keyStatus[78]) return "";//n
            if(keyStatus[77]) return "";//m
            if(keyStatus[188]) return "";//,
            if(keyStatus[190]) return "";//.
            if(keyStatus[191]) return "";///
            return "";//v
        }
        if(keyStatus[77]){//右手半濁音キー(m)押し
            if(keyStatus[81]) return "";//q
            if(keyStatus[87]) return "";//w
            if(keyStatus[69]) return "";//e
            if(keyStatus[82]) return "";//r
            if(keyStatus[73]) return "";//i
            if(keyStatus[79]) return "";//o
            if(keyStatus[80]) return "";//p
            if(keyStatus[65]) return "";//a
            if(keyStatus[83]) return "";//s
            if(keyStatus[68]) return "";//d
            if(keyStatus[70]) return "";//f
            if(keyStatus[71]) return "";//g
            if(keyStatus[72]) return "";//h
            if(keyStatus[74]) return "";//j
            if(keyStatus[75]) return "";//k
            if(keyStatus[76]) return "";//l
            if(keyStatus[186]) return "";//;
            if(keyStatus[90]) return "";//z
            if(keyStatus[88]) return "";//x
            if(keyStatus[67]) return "";//c
            if(keyStatus[86]) return "";//v
            if(keyStatus[66]) return "";//b
            if(keyStatus[78]) return "";//n
            if(keyStatus[188]) return "";//,
            if(keyStatus[190]) return "";//.
            if(keyStatus[191]) return "";///
            return "";//m
        }
        if(keyStatus[81]) {
            if(keyStatus[74]) return "ヴぁ";
            if(keyStatus[75]) return "ヴぃ"
            return "";//q
        }
        if(keyStatus[87]) return "";//w
        if(keyStatus[69]) {
            if(keyStatus[75]) return "てぃ";
            return "";//e
        }
        if(keyStatus[82]) return "";//r
        if(keyStatus[73]) return "";//i
        if(keyStatus[79]) return "";//o
        if(keyStatus[80]) return "";//p
        if(keyStatus[65]) return "";//a
        if(keyStatus[83]) return "";//s
        if(keyStatus[68]) {
            if(keyStatus[76]) return "とぅ"
            return "";//d
        }
        if(keyStatus[70]) return "";//f
        if(keyStatus[71]) return "";//g
        if(keyStatus[72]) return "";//h
        if(keyStatus[74]) return "";//j
        if(keyStatus[75]) return "";//k
        if(keyStatus[76]) return "";//l
        if(keyStatus[186]) return "";//;
        if(keyStatus[90]) return "";//z
        if(keyStatus[88]) return "";//x
        if(keyStatus[67]) return "";//c
        if(keyStatus[86]) return "";//v
        if(keyStatus[66]) return "";//b
        if(keyStatus[78]) return "";//n
        if(keyStatus[77]) return "";//m
        if(keyStatus[188]) return "";//,
        if(keyStatus[190]) return "";//.
        if(keyStatus[191]) return "";///
    }
}

タイピングゲームの基本機能
document.onkeydown = keydownFunction;  //キー押下時に関数keydownFunction()を呼び出す
document.onkeyup = releaseFunction;

var originSentence = "";
var count=0;             //何問目か格納
var typStart,typEnd;   //開始時と終了時の時刻を格納
var SentenceLength=0;
var typeLetter ="";
var keyStatus = new Array();
var SentenceArray = new Array("")//問題文をべた書きしているため省略

//タイピングゲームの問題をセットする関数
function SetSentence()
{
    //問題文とカウント数をクリアする
    count=0;
    SentenceLength=Sentence.length;
    originSentence=Sentence;
    //問題枠に表示する
    document.getElementById("waku").style.backgroundColor="aliceblue";
    document.getElementById("waku").innerHTML = originSentence;
}

function keydownFunction(e)
{
    //入力されたキーのキーコードを取得
    if(e.keyCode>30){   
        keyStatus[e.keyCode]=true;
        typeLetter = reaction(keyStatus);
    }
    return false;
}

function releaseFunction(e){
    if(typeLetter==Sentence.charAt(0)||typeLetter==Sentence.slice(0,2)){
        if (count==0)
        { 
            typStart = new Date();
        }
        count++;

        document.getElementById("waku").style.backgroundColor="aliceblue";
        if (count < SentenceLength)
        {
            //問題文の頭の一文字を切り取る
            if(typeLetter==Sentence.slice(0,2)){
                sliceSentence=Sentence.substring(0,2);
                Sentence = Sentence.slice(2);
                count++;
            }
            else{
                sliceSentence=Sentence.substring(0,1);
                Sentence = Sentence.slice(1);
            }

            if(Sentence.charAt(0)==" "){
                Sentence = Sentence.slice(1);
                count++;
            }
            //問題枠に表示する
            document.getElementById("pushKey").innerText="入力された文字:" + typeLetter;
            document.getElementById("waku").style.backgroundColor="palegreen";
            var aliceblue=function(){document.getElementById("waku").style.backgroundColor="aliceblue"}
            setTimeout(aliceblue,200);
            document.getElementById("waku").innerHTML = Sentence;
        }    
        else
        {
        //全文字入力していたら、終了時間を記録する
        typEnd = new Date();

        //終了時間-開始時間で掛かったミリ秒を取得する
        var elapsed = typEnd - typStart;

        //1000で割って「切捨て」、秒数を取得
        var sec = Math.floor( elapsed/1000 );

        //1000で割った「余り(%で取得できる)」でミリ秒を取得
        var msec = elapsed % 1000;

        //問題終了を告げる文字列を作成
        var fin="GAME終了 時間:"+sec+""+msec+"\n";

        //問題枠にゲーム終了を表示
        document.getElementById("waku").innerHTML = fin;
        }
    }
    else{
        if(typeLetter!=undefined){
            document.getElementById("pushKey").innerText="入力された文字:" + typeLetter;  
        }
        else document.getElementById("pushKey").innerText="入力された文字:"
    } 
    keyStatus.map(function(value, index, array){
        return array[index]=false;
    });
    return false;
}

htmlのbody部分

タイピングゲーム

下のプルダウンメニューからLessonを選択してください


右手心臓部1
左手心臓部1
両手心臓部2
センターシフト
短文
半濁音、小書き
拗音
拗音2、外来音
その他
マイナー音(ふ)
マイナー音(ほ)
マイナー音(せ)
マイナー音(へ)
マイナー音(ヴ)
半濁音練習
人差し指内側キー濁音
左手マイナーキー




入力された文字:




配列図


薙刀式配列図

実装としては、入力されたキーコード部分がtrueになる配列を用意して(同時押しが存在するため)、条件分岐して薙刀式で入力した場合の文字列として返し、問題文と比較して合っていたら問題文を削るというものです。薙刀式の条件分岐を作成するのに時間がかかりました。

正解すると画面が黄緑色になるよう実装しています。理由は、不正解時に赤くなるようにしていた場合同時押しにすると無条件で赤くなってしまう問題に直面したからです。配列で比較しているために問題が起こっているのでしょうか。自分では解決に至りませんでしたので正解で黄緑色になるようにしました。

終わりに

キーコードを取得して、実際に入力されるものを変えるという処理を応用すれば他の配列もタイピングソフトを作成することは可能なので、作ってみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascript で再開可能アップロードを使って大きなサイズのファイルを非同期で Google Drive へアップロードする

背景と概要

大きなサイズのファイル(5 MB 以上あるいは 50 MB 以上)を Google Drive へアップロードしようとすると、再開可能アップロード(Resumable upload)のプロセスを行う必要があります。Javascript を使用してブラウザからファイルをアップロードするとき、多くの場合は公式のライブラリであるgoogle-api-javascript-clientを使用します。この場合、Google Docs ファイルの新規作成や 5 MB 以下のファイルをアップロードすることは可能ですが、この記事を書いた段階では再開可能アップロードを行うためのメソッドはまだ用意されていません。そこで、Javascript を使って大きなサイズのファイルを再開可能アップロードプロセスでアップロードするためのライブラリを作成しました。

ここでは、このライブラリを使用したサンプルとして、大きなサイズのファイル(100 MB)を 5 つ用意し、これを非同期で Google Drive へアップロードするスクリプトを紹介させていただきます。

ResumableUploadForGoogleDrive_js

ResumableUploadForGoogleDrive_js は、Drive API を使用してファイルを再開可能アップロードするための Javascript 用ライブラリです。リポジトリはこちらです。詳細な使用方法は、リポジトリをご覧ください。

使い方

1. スプレッドシートの作成

ここでは、スプレッドシートを作成し、スプレッドシート上へ開いたサイドバーでスクリプトを実行させます。そのため、最初にスプレッドシートを作成してください。作成後、スクリプトエディタを開いてください。

2. サンプルスクリプトをコピーペースト

Google Apps Script と HTML&Javascript の 2 種類のスクリプトを使用します。そのため、スクリプトエディタを開いた際、ファイル->新規作成->HTML ファイル として HTML ファイルを作成してください。ファイル名はindex.htmlです。

Google Apps Script: コード.gs

下記のスクリプトをコード.gsへコピーペーストします。

function main() {
  var html = HtmlService.createHtmlOutputFromFile("index");
  SpreadsheetApp.getUi().showSidebar(html);
}

function getAuth() {
  // DriveApp.createFile(blob) // This is used for adding the scope of "https://www.googleapis.com/auth/drive".
  return ScriptApp.getOAuthToken();
}

HTML: index.html

下記のスクリプトを作成したindex.htmlへコピーペーストします。ライブラリは、jsdelivr cdn を使ってロードしています。また、アクセストークンは Google Apps Script 側で取得しています。

<input type="file" id="file" multiple="true" />
<input type="button" onclick="run()" value="Upload" />
<div id="progress"></div>

<script src="https://cdn.jsdelivr.net/gh/tanaikech/ResumableUploadForGoogleDrive_js@master/resumableupload_js.min.js"></script>
<script>
  function run() {
    google.script.run
      .withSuccessHandler(accessToken =>
        ResumableUploadForGoogleDrive(accessToken)
      )
      .getAuth();
  }

  function ResumableUploadForGoogleDrive(accessToken) {
    const f = document.getElementById("file");
    [...f.files].forEach((file, i) => {
      let fr = new FileReader();
      fr.fileName = file.name;
      fr.fileSize = file.size;
      fr.fileType = file.type;
      fr.readAsArrayBuffer(file);
      fr.onload = e => {
        var id = "p" + ++i;
        var div = document.createElement("div");
        div.id = id;
        document.getElementById("progress").appendChild(div);
        document.getElementById(id).innerHTML = "Initializing.";
        const f = e.target;
        const resource = {
          fileName: f.fileName,
          fileSize: f.fileSize,
          fileType: f.fileType,
          fileBuffer: f.result,
          accessToken: accessToken
        };
        const ru = new ResumableUploadToGoogleDrive();
        ru.Do(resource, function(res, err) {
          if (err) {
            console.log(err);
            return;
          }
          console.log(res);
          let msg = "";
          if (res.status == "Uploading") {
            msg =
              Math.round(
                (res.progressNumber.current / res.progressNumber.end) * 100
              ) +
              "% (" +
              f.fileName +
              ")";
          } else {
            msg = res.status + " (" + f.fileName + ")";
          }
          document.getElementById(id).innerText = msg;
        });
      };
    });
  }
</script>

3. スクリプトの実行

スクリプトエディタから ファンクション main() を実行すると、スプレッドシート上へサイドバーが表示されます。ここでファイルを選択して Upload ボタンを押すと、ファイルがアップロードされます。下記はデモンストレーションの動画です。

20191021a-demo.gif

また、サンプルスクリプトのリポジトリはこちらです。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JSON内の特定の値をマスクする

 目的

APIリクエスト・レスポンスをログに吐きだす際など用のメモ書き

javascript

const MASK_KEYS = ['email', 'password']

const maskJson = obj =>
  MASK_KEYS.reduce(
    (memo, key) =>
      memo.replace(new RegExp(`"${key}":"[^,}]*"`), `"${key}":"XXXXXX"`),
    JSON.stringify(obj)
  )

console

maskJson({ email: 'sample@email.com', password: 'password' })
=> "{"email":"XXXXXX","password":"XXXXXX"}"

ruby

MASK_KEYS = %w[email password].freeze

def mask_json(hash)
  MASK_KEYS.inject(JSON.generate(hash)) { |memo, key| memo.gsub(/("#{key}":"[^,}]*")/, "\"#{key}\":\"XXXXXX\"") }
end

console

mask_json({ email: 'sample@email.com', password: 'password' })
=> "{\"email\":\"XXXXXX\",\"password\":\"XXXXXX\"}"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript

ステートメントについて

1つの命令の事をステートメントと呼ぶ
(例)
alert("ようこそJavaScriptの世界へ");

alert:ダイアログに表示するタグ
"ようこそ~":ダイアログ内に表示される
「;」:ステートメントの終わりにつける。

  • コメントアウト

    • // コメントアウトしたい文
    • /* コメントアウトしたい文 */
  • メソッドの呼び方

    • 「オブジェクト名.メソッド名(引数);」
    • 引数が複数ある場合は「,」で区切って指定する。
  • プロパティを記述するときも「オブジェクト名.プロパティ名」

  • プロパティに値を設定する時

    • (ex) document.bgColor = "yellow";
  • しかし document.[bgColor] = "yellow";でも接続可能。好きな方でOK

※ プロパティ名の大文字小文字は区別されているから正確に記述する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのステートメントとは

ステートメントについて

1つの命令の事をステートメントと呼ぶ
(例)
alert("ようこそJavaScriptの世界へ");

alert:ダイアログに表示するタグ
"ようこそ~":ダイアログ内に表示される
「;」:ステートメントの終わりにつける。

  • コメントアウト

    • // コメントアウトしたい文
    • /* コメントアウトしたい文 */
  • メソッドの呼び方

    • 「オブジェクト名.メソッド名(引数);」
    • 引数が複数ある場合は「,」で区切って指定する。
  • プロパティを記述するときも「オブジェクト名.プロパティ名」

  • プロパティに値を設定する時

    • (ex) document.bgColor = "yellow";
  • しかし document.[bgColor] = "yellow";でも接続可能。好きな方でOK

※ プロパティ名の大文字小文字は区別されているから正確に記述する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript

随時追記します

ソースプログラムのマシン語の変換方式

  • コンパイラ方式
    • 「コンパイラ」っていうソフトウェア使って一気に変換を行う
  • インタプリタ方式
    • 1行ずつ変換していく

JavaScriptインタプリタ方式だよ

Java、C#、Python とかは本格的なオブジェクト指向の概念を取り入れている
JavaScriptは簡単なオブジェクト指向を取り入れている⇒オブジェクトベース指向

⇒ 初心者に優しいよ!!!!!!!

JavaScriptに必要なものは?

  • エディタ(メモ帳とかサクラエディタとか)
  • エディタの文字コード⇒UTF-8にする
  • プログラムを動かすにはWebブラウザを使用

必要なことその2

  • 2つの<'meta>タグが必要
    • プログラミング言語の指定
    • 文字コードの指定
  • <'script>タグのtype属性は、「text/javascript」を指定
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの概要

随時追記します

ソースプログラムのマシン語の変換方式

  • コンパイラ方式
    • 「コンパイラ」っていうソフトウェア使って一気に変換を行う
  • インタプリタ方式
    • 1行ずつ変換していく

JavaScriptインタプリタ方式だよ

Java、C#、Python とかは本格的なオブジェクト指向の概念を取り入れている
JavaScriptは簡単なオブジェクト指向を取り入れている⇒オブジェクトベース指向

⇒ 初心者に優しいよ!!!!!!!

JavaScriptに必要なものは?

  • エディタ(メモ帳とかサクラエディタとか)
  • エディタの文字コード⇒UTF-8にする
  • プログラムを動かすにはWebブラウザを使用

必要なことその2

  • 2つの<'meta>タグが必要
    • プログラミング言語の指定
    • 文字コードの指定
  • <'script>タグのtype属性は、「text/javascript」を指定
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jQueryでHTMLのツリーを編集する

結局使わなかったコードをお焚き上げ。
<link>タグの追加と指定したタグ削除をjQueryで行うコードです。
リストが6件以下だったらタグ消してCSS1読み込んで、違ったらCSS2読み込む、といった流れです。

<script type="text/javascript">
    $(function() {
    if (6 >= $('#foo_list').children().length ) {
        // リストが6件以下

        // CSS1を読込む
        $('head').append('<link rel="stylesheet" href="css/style_1.css" media="all" />');

        // タグの削除
        $('#bar_list').remove();
        $('#hoge_list > label').remove();
    } else {
        // リストが6件以上

        // CSS2を読込む
        $('head').append('<link rel="stylesheet" href="css/style_2.css" media="all" />');
    }
    });
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

discord.js で Bot を作成して Heroku で永続稼働させる

環境

  • macOS Mojave 10.14.3

事前準備

Homebrew のインストール

https://qiita.com/naente_dev/items/1195ff834c65be4be5a6

nodebrew および Node.js のインストール

https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09
こちらの記事を参考に導入しました。

Discord Bot の登録

Discord Developer Portal ページにアクセスします。
FireShot Capture 028 - Discord Developer Portal — My Applications - discordapp.com.png
New Application ボタンをクリックします。
FireShot Capture 029 - Discord Developer Portal — My Applications - discordapp.com.png
NAME 欄に任意のアプリケーション名を入力して Create ボタンをクリックします。
FireShot Capture 030 - Discord Developer Portal — My Applications - discordapp.com.png
アプリケーションの作成に成功すると General Information ページにリダイレクトされます。
左側の SETTINGS メニューから Bot を選択します。
FireShot Capture 031 - Discord Developer Portal — My Applications - discordapp.com.png
Add Bot をクリックします。
FireShot Capture 032 - Discord Developer Portal — My Applications - discordapp.com.png
Yes, do it! をクリックします。
FireShot Capture 033 - Discord Developer Portal — My Applications - discordapp.com.png
Bot が追加されました。

TOKEN 欄の Copy ボタンをクリックして、Bot のトークンをコピーします。
開発時に使用するので、どこかに控えておきます。

続いて、左側の SETTINGS メニューから OAuth2 を選択します。
FireShot Capture 034 - Discord Developer Portal — My Applications - discordapp.com.png
OAuth2 URL Generator - SCOPE 欄にある bot のチェックボックスを選択すると
オーソライズ URL が表示されるのでコピーして、アクセスします。
FireShot Capture 035 - アカウントへのアクセスを許可します - discordapp.com.png
Bot を追加したいサーバーを選択して 認証 ボタンをクリックします。

Bot を追加できるのは自身が管理者権限を所持しているサーバーに限定されます。
権限の無いサーバーは一覧に表示されません。
スクリーンショット 2019-10-20 11.28.34.png
スクリーンショット 2019-10-20 11.29.15.png
サーバーに Bot が追加されましたが、この時点ではまだオフラインです。

開発

例として MyDiscordBot というディレクトリを作成して、その中で開発を進めていきます。

$ mkdir MyDiscordBot
$ cd MyDiscordBot/

discord.js のインストール

https://discord.js.org/

$ npm install discord.js
略
+ discord.js@11.5.1
added 7 packages from 6 contributors and audited 7 packages in 2.183s
found 0 vulnerabilities

成功するとカレントディレクトリ内に
discord.js および依存ライブラリを含む node-modules ディレクトリと
package-lock.json ファイルが作成されます。

コーディング

MyDiscordBot ディレクトリの直下へ index.js を作成します。

以下、公式リファレンスに記載されている ping と発言すると Pong! とリプライしてくれる Bot の
コードとなります。
(トークン部分のみ定数化しました)

MyDiscordBot/index.js
const Discord = require('discord.js');
const client = new Discord.Client();
const BOT_TOKEN = '***********************************************************';

client.on('ready', () => {
    console.log(`Logged in as ${client.user.tag}!`);
});

client.on('message', msg => {
    if (msg.content === 'ping') {
        msg.reply('Pong!');
    }
});

client.login(BOT_TOKEN);

BOT_TOKEN には Discord Developer Portal ページで取得した Bot のトークンを
設定してください。

Node.js ローカルサーバーで Bot を起動

$ node index.js
Logged in as Application Name#0000!

スクリーンショット 2019-10-20 12.54.29.png
Bot がオンラインになりました。

スクリーンショット 2019-10-20 12.57.51.png
ping と発言すると Pong! とリプライしてくれます。

デプロイ

このままでは自身の端末で Bot を起動している時のみしか利用できない為、
今回は Heroku にデプロイしてみます。

Heroku を利用した永続起動

https://qiita.com/InkoHX/items/590b5f15426a6e813e92

こちらの記事を参考に Heroku への登録〜デプロイまでを実施しました。

補足

$ git push heroku origin --force

デプロイしようの箇所に記載されている上記のコマンドではプッシュできなかった為、
下記コマンドで対応しました。

$ git push heroku master --force

Heroku タイムゾーンの設定

Heroku のデフォルトタイムゾーンは UTC(協定世界時)ですので
日本時間と 9時間の遅れがあります。

タイマー処理など時刻に関する処理を実装する予定があれば JST(日本標準時)に変更しておきましょう。

https://qiita.com/ikemura23/items/52ab8a5d260c7ee4d42b

こちらの記事を参考に設定させていただきました。

Heroku コマンド抜粋

$ heroku ps

dyno 無料分の残時間や使用時間、Web/Worker の稼働状態を確認できます。

$ ps:scale worker=0

Worker dyno のスケール数を 0 にできます。

$ ps:scale worker=1

Worker dyno のスケール数を 1 にできます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptにおける「データ型」の判定メモ

―令和元年のある日、とあるぽっぽこエンジニアが、先人たちのコードを読んでいて「??」となったことがあったらしい。

「データ型の判定で、ここではtypeofじゃなくてObject.prototype.toString.call()が使われているけど、なぜだろう…?」
「ていうか、こいつらなにが違うん…?」

これは、そんな疑問からはじまったJavaScriptにおけるデータ型の判定についての探検をメモしたものである。

データ型を判定する方法

  • typeof演算子を使う
  • Object.prototype.toString.call()を使う
  • その他の方法

typeof演算子を使う

JavaScriptの入門書とかによく書かれている方法…?データ型判定だとなんだかよく見るやつ。
ここで注意したいのは、MDN の typeofに書かれているように

構文
typeof 演算子の後に、オペランドを続けて書きます。
typeof operand

引数
operand は、オブジェクトもしくは primitive 型を返す式を表します。

なのである。

ほう…「オペランドは、オブジェクトもしくはプリミティブ型を返す式を表す」とな。

typeof演算子で判別したらざっくりとこんな感じ↓↓↓
(JavaScriptのデータ型は、オブジェクト型プリミティブ型の2つに大きくわけられるそう。)

分類 データ型 概要 戻り値
プリミティブ型 数値 数値 / NaN / Infinity など number
文字列 文字列 string
真偽値 true / false boolean
null null object(※1)
undefined 未定義 undefined
オブジェクト型 配列 [ index : 'value' ] object
マップ key, valueのセット object
セット 重複しない値の集合 object
オブジェクト { key : 'value' } object
関数 function() {} / Math.round など function(※2)

※1:nullがなぜ'object'判定に…?それはjsの仕様上、仕方のないことだそうで。
※2:関数オブジェクトだけは特別に'function'を返す。

前提として、
プリミティブ型:プロパティとメソッドを持たない単純なデータ(1 や 'Hello' や true)
オブジェクト型:プロパティとメソッドの集まり
な感じ。ざっくり。

MDN曰く

コンストラクター関数はすべて、関数コンストラクターを除いて、常に typeof 'object' になります

とのこと。
new演算子でインスタンス化すると、prototypeを参照できるオブジェクトになるので…って感じ。

よって、以下の判定はすべてobjectになってしまう。

const hoge = new Number(10);
const fuga = new Boolean(true);
const piyo = new Date();

console.log(typeof hoge);
console.log(typeof fuga);
console.log(typeof piyo);

ここでちょっとややこしくなるが、ラッパーオブジェクトという存在に触れておきたい。(メモしておきたいだけ)

たとえば、

console.log('ねこさん');

ってやったら単純に「ねこさん」が返ってくる。そりゃそうだ。

でも、

console.log(new String('ねこさん'));

ってやると、なんて返ってくるのか…?(Google Chrome の console でやるのがおすすめ)

答えは…

String {0: "", 1: "", 2: "", 3: "", length: 4, [[PrimitiveValue]]: "ねこさん"}

…はて?

簡単に説明すると
このようにプリミティブ型をnew演算子でインスタンス化すると、プリミティブ型のデータをラップしたオブジェクトとなる。これがラッパーオブジェクト

なので、console.log(typeof new String('ねこさん'));とかやると、ラッパーオブジェクトの名の通り、objectの判定になる。

よって、以下の判定はすべてobject

console.log(typeof new String('ねこさん'));
console.log(typeof new Number(10));
console.log(typeof new Boolean(true));

ちなみに
インスタンス化せずに普通に文字列リテラルで生成したときは、プロパティとかにアクセスするときだけ一時的に裏側でStringクラスの機能を持ったオブジェクトでラップする。
そうしてオブジェクトとしての機能へアクセスできるようになるそうだ。

…話を戻して、ここまで見てきて感じたと思うが、typeof演算子での判定ってなんかこう…ふわっとしている………。
配列とオブジェクトの区別できんやん…!とか。

じゃあ、もっと詳しく判定する方法へ。

Object.prototype.toString.call() を使う

Object.prototype.toString()[object type](typeはオブジェクト型)といった形で、いわゆるクラスを返してくれる。
よく見るようなサンプルコードで見た方が早いので↓↓↓

const toString = Object.prototype.toString;

toString.call(1234);          // [object Number]
toString.call(Infinity);      // [object Number]
toString.call(NaN);           // [object Number]
toString.call('hoge');        // [object String]
toString.call(true);          // [object Boolean]
toString.call([]);            // [object Array]
toString.call(new Map());     // [object Map]
toString.call(new Set());     // [object Set]
toString.call({});            // [object Object]
toString.call(function() {}); // [object Function]

toString.call(new String('ねこさん')); // [object String]
toString.call(new Number(10));       // [object Number]
toString.call(new Boolean(true));    // [object Boolean]
toString.call(new Date());           // [object Date]

toString.call(Math); // [object Math]
toString.call(JSON); // [object JSON]

// ECMAScript5 から?
toString.call(null);      // [object Null]
toString.call(undefined); // [object Undefined]

call()メソッドは言うまでもないが
function.call(引数)で、引数があたかも自分のメソッドかのようにfunctionを使っちゃうメソッド。(雑)

上記の例だと、注意すべきはNaNくらい…?

あと、自作のコンストラクタから生成したオブジェクトは[object Object]が返ってくる。
でも考えてみれば当たり前のような………

その他の方法

その他の方法だと、Array.isArray()isNaN()Number.isNaN()あたりだろうか。

Array.isArray()

これは単純に、渡された値が配列かどうかを真偽値で返してくれる。

Array.isArray([]));          // true
Array.isArray(new Array())); // true
Array.isArray({}));          // false
Array.isArray(new Map()));   // false
Array.isArray(new Set()));   // false

isNaN() 、 Number.isNaN()

NaNの判定といえばisNaN()とか。
じゃあ、isNaN()Number.isNaN()の違いってなんでござろう?

isNaN()
これは渡された引数を数値に変換してから判定するメソッド。
引数が数値へと強制変換された後にNaNであるかを確認するため、プログラム上NaN以外の数値を〜ってなところに、うっかり0や1に変換されてしまう空文字列や真偽値を渡してしまうと'false'と返される。要するにNaNじゃない数値だよ〜って判断してしまったりする。
例として以下のような判定がされてしまう………なんてことだ…。

console.log(isNaN(NaN));       // true
console.log(isNaN(undefined)); // true
console.log(isNaN({}));        // true
console.log(isNaN('hoge'));    // true
console.log(isNaN(''));        // false
console.log(isNaN(true));      // false
console.log(isNaN(null));      // false

Number.isNaN()
比べてこちらは、引数が数値型であり、かつ、NaNであるもののみtrueを返すメソッド。
いずれかを満たさなければfalseを返すので、心配なし。
ただ、ES6から新たに導入されたので環境次第では使えない場合も…。

console.log(Number.isNaN(NaN));       // true
console.log(Number.isNaN(undefined)); // false
console.log(Number.isNaN({}));        // false
console.log(Number.isNaN('hoge'));    // false
console.log(Number.isNaN(''));        // false
console.log(Number.isNaN(true));      // false
console.log(Number.isNaN(null));      // false

最後に

結論としては、データ型判定はObject.prototype.toString.call()を使うのがベストかな…

まだまだぽっぽこエンジニアなので、間違いなどあればご教示いただけると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

contact form 7 でクッキー情報を送信する

contact form 7 でクッキー情報を送信する方法を試行錯誤しながら書いたので備忘録的な感じで書いていきます。

なぜやろうと思ったか

元々の機能としては投稿をブックマークしてそのブックマークをしたものをformで送信するというものを作成するつもりでした。

しかし、ブックマークするプラグインは溢れているのにも関わらず、それをformで送信する機能のついたプラグインは確認できませんでした。

それならいっそ作ってしまおうと、そういうことです。

functions.php
add_action( 'get_header', 'fovorites_form');

function fovorites_form() {
    if(!is_NULL($_COOKIE["simplefavorites"])){
        preg_match('/(?<=^.{27}).+/', $_COOKIE["simplefavorites"], $str);
            $splited = str_split($str[0]);  
                $i = 0;
        $j = 0;

        $nos[0] = '';
        while($splited[$j] != ']'){
            if($splited[$j] == ','){
                $i++;
                $nos[$i] = '';
            }
            else{
                $nos[$i] .= $splited[$j];
            }
            $j++;
        }
        global $wpdb;
        $sql = '';
        foreach ($str as $value) {
                $sql .= $value;
                $sql .= ',';
        }
        $sql = substr($sql, 0, -1);
        $result = $wpdb->get_results("SELECT post_title, guid FROM wp_posts WHERE ID IN ($sql)", ARRAY_A );
        $i = 0;
        foreach ($result as $value) {
            $json[$i]['post_title'] = $value['post_title'];
            $json[$i]['post_guid'] = $value['guid'];
            $i++;
        }
        wp_enqueue_script('some_handle', 'jsファイルのパス', array( 'jquery' ), false, true);
        wp_localize_script('some_handle', 'object_name', $js_variable);
    }
}

全体像はこんな感じです。
jsファイルは

main.js
jQuery(function(){
  if(jQuery('#documents').length != 0){
    if(object_name){
      var checkbox = jQuery('#documents input').html();
      var input;
      for (var i = 0; i < object_name.length; i++) {
        input = '<input type="checkbox" name="' + 'checkbox-455[]' + '" value="' + object_name[i]['post_title'] + '" checked="checked"><a href="' + object_name[i]['post_guid'] + '">' + object_name[i]['post_title'] + '</a><br>';
        jQuery('#documents').append(input);
       }
    }
    else
    {
          jQuery('#documents').append('<p>...</p>');
    }
  }
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Markdown表記での数式の変換ツール作ったのでぜひ使ってもらいたい

こんなものを作りました

5484bb87d9f5fc356303b26eeb204b98.gif

Qiita、Kibela、はてなブログで書く記事のなかに数式を埋め込みたいという時があります。
例えばQiita記事の中に数式をおりこんで記事をかいたとして、Kiberaでも同じ内容や数式を使って記事を書きたい。
そう思いたった時、それぞれのサービスに合わせたフォーマットで数式を書き直す必要があります。

QiitaにはQiitaの数式の書き方があるし、、KibelaにはKibelaの書き方がある。。。(なんて面倒なの・・・)

そういった面倒な作業をなくすための数式変換ツールを作成したので、ぜひ使っていただけたらと思います。

▽github.ioで公開しています▽(※スマホでは崩れます。)
https://hikariyamasaki.github.io/markdown_Formula/

▽GitHubレポジトリはこちら▽
https://github.com/hikariyamasaki/markdown_Formula

ソースコード

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Formula</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.3.1/jquery.mobile-1.3.1.min.css" />
    <link rel="stylesheet" href="reset.css">
    <link rel="stylesheet" href="style.css">
    <script src="script.js"></script>
  </head>
  <body>
    <div>
      <div data-role="header"></div>
      <div class="content-wrapper">
        <div class="content">
          <div>
            <h1 class="title">Formula</h1>
          </div>
          <div class="code">
            <form id="inputform">
              <textarea id="input"></textarea>
            </form>
          </div>
          <div class="code-format">
            <form name="source">
                <select name="source">
                  <option value="qiita">Qiita</option>
                  <option value="kibela">Kibela</option>
                  <!-- <option value="hatena">はてなブログ</option> -->
                </select>
              </form>
              <p><img src="./svg/autorenew.svg" class="icon-autorenew"></p>
              <form name="target">
                <select name="target">
                  <option value="qiita">Qiita</option>
                  <option value="kibela">Kibela</option>
                  <option value="hatena">はてなブログ</option>
                </select>
              </form>
              <button id="change" onclick="transform_text()">変換</button>
          </div>
          <div class="code">
              <form id="outputform">
                <textarea id="output"></textarea>
              </form>
          </div>
        </div>
     /* サイドは省略 */
    </div>
  </body>
</html>
script.js
function checkFormat(value, source, target){
  if(value.match(source.start)) {
    var preresult = value.replace(source.start, target.start);

    var index = value.indexOf(source.start);
    value = value.slice(index + source.start.length + 1);

    if(preresult.match(source.end)) {
      var result = value.replace(source.end, target.end);
    }

    var preresult = preresult.slice(0, index+target.start.length+1);
    document.getElementById("output").value = preresult + result;
    return preresult + result;
  }
  return false;
}

function transformation(source, target) {
  var inputValue = document.getElementById("input").value ;

  if(!inputValue) {
    alert("値を入力してください");
  } else {
    var value = inputValue;

    const count_max=100;
    for(let i=0; i < count_max; i++){
      value = checkFormat(
        value,
        source,
        target
      );
      if(!value) break;
    }
  }
}

function transform_text(){
  var source_type = document.source.source.value;
  var target_type = document.target.target.value;

  let source = getFormat(source_type);
  let target = getFormat(target_type);

  transformation(source, target);
}

function getFormat(texttype) {
  switch(texttype) {
    case 'qiita':
    return {start: "```math", end:"```"};
    break;
    case 'kibela':
    return {start: "~~~{latex}" , end: "~~~"};
    break;
    case 'hatena':
    return {start: "[tex:", end: "]"};
    break;
  }
}

考えた手順

それぞれの文法には以下のルールがあります。
【数式開始の文言】
Qiitaの文法 ```math
Kibelaの文法 ~~~{latex}
はてなブログの文法 [tex:

【数式終了の文言】
Qiitaの文法 ```
Kibelaの文法 ~~~
はてなブログの文法 ]

考え方としては、例えば、Qiitaのフォーマットで書いたテキストをKibelaフォーマットに変換したい場合、入力したテキストの中に```mathという文字列があれば、~~~{latex}へ変換。```という文字列があれば~~~へ変換する、という考え方です。

では、入力したテキスト中の全ての```math~~~{latex}に置換して、そして同じように```~~~へ全置換しよう。JavaScriptのreplaceを使えば一発OK!

そう考えてしまうかもしれませんが、このやり方だと以下のようなテキストの場合うまくいきません。



   ```javascript
   fuga
   ```

   ```math
   hogehoge
   ```
   text text text text.
   ```math
   hogehoge
   ```


例えば、このようなテキストの場合、全ての```mathを置換してしまったあと、残りの```を置換しようとしても、どの``````mathとセットになっているものかわからないため、置換する必要がないjavascriptの終始記号をも置換してしまいます。

そのため、置換を実行する時は、```mathを置換し、そのあとに現れる```~~~に置換するという実装にする必要があります。
Qiita→Kibelaであれば上の実装ですが、Qiita→はてなブログ、Kibela→Qiita、Kibela→はてなブログ。
どの変換も置換する文字列が違うだけで、あとの実装の流れは一緒です。

課題

今回の実装では、文中に数式を埋め込む文字列の変換が実装できていません。
そしてはてなブログ→{Qiita, Kibera}の変換がまだ実装できていないのでぜひやり方をおしえて欲しいです。。

★数式変換ツールのレポジトリはこちら★

また、このような変換サービスが既存であるよなどの有力情報があればぜひ教えてほしいです。

おわり

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでMarkdown表記での数式の変換ツール作ったのでぜひ使ってもらいたい

こんなものを作りました

5484bb87d9f5fc356303b26eeb204b98.gif

Qiita、Kibela、はてなブログで書く記事のなかに数式を埋め込みたいという時があります。
例えばQiita記事の中に数式をおりこんで記事をかいたとして、Kiberaでも同じ内容や数式を使って記事を書きたい。
そう思いたった時、それぞれのサービスに合わせたフォーマットで数式を書き直す必要があります。

QiitaにはQiitaの数式の書き方があるし、、KibelaにはKibelaの書き方がある。。。(なんて面倒なの・・・)

そういった面倒な作業をなくすための数式変換ツールを作成したので、ぜひ使っていただけたらと思います。

▽github.ioで公開しています▽(※スマホでは崩れます。)
https://hikariyamasaki.github.io/markdown_Formula/

▽GitHubレポジトリはこちら▽
https://github.com/hikariyamasaki/markdown_Formula

ソースコード

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Formula</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.3.1/jquery.mobile-1.3.1.min.css" />
    <link rel="stylesheet" href="reset.css">
    <link rel="stylesheet" href="style.css">
    <script src="script.js"></script>
  </head>
  <body>
    <div>
      <div data-role="header"></div>
      <div class="content-wrapper">
        <div class="content">
          <div>
            <h1 class="title">Formula</h1>
          </div>
          <div class="code">
            <form id="inputform">
              <textarea id="input"></textarea>
            </form>
          </div>
          <div class="code-format">
            <form name="source">
                <select name="source">
                  <option value="qiita">Qiita</option>
                  <option value="kibela">Kibela</option>
                  <!-- <option value="hatena">はてなブログ</option> -->
                </select>
              </form>
              <p><img src="./svg/autorenew.svg" class="icon-autorenew"></p>
              <form name="target">
                <select name="target">
                  <option value="qiita">Qiita</option>
                  <option value="kibela">Kibela</option>
                  <option value="hatena">はてなブログ</option>
                </select>
              </form>
              <button id="change" onclick="transform_text()">変換</button>
          </div>
          <div class="code">
              <form id="outputform">
                <textarea id="output"></textarea>
              </form>
          </div>
        </div>
     /* サイドは省略 */
    </div>
  </body>
</html>
script.js
function checkFormat(value, source, target){
  if(value.match(source.start)) {
    var preresult = value.replace(source.start, target.start);

    var index = value.indexOf(source.start);
    value = value.slice(index + source.start.length + 1);

    if(preresult.match(source.end)) {
      var result = value.replace(source.end, target.end);
    }

    var preresult = preresult.slice(0, index+target.start.length+1);
    document.getElementById("output").value = preresult + result;
    return preresult + result;
  }
  return false;
}

function transformation(source, target) {
  var inputValue = document.getElementById("input").value ;

  if(!inputValue) {
    alert("値を入力してください");
  } else {
    var value = inputValue;

    const count_max=100;
    for(let i=0; i < count_max; i++){
      value = checkFormat(
        value,
        source,
        target
      );
      if(!value) break;
    }
  }
}

function transform_text(){
  var source_type = document.source.source.value;
  var target_type = document.target.target.value;

  let source = getFormat(source_type);
  let target = getFormat(target_type);

  transformation(source, target);
}

function getFormat(texttype) {
  switch(texttype) {
    case 'qiita':
    return {start: "```math", end:"```"};
    break;
    case 'kibela':
    return {start: "~~~{latex}" , end: "~~~"};
    break;
    case 'hatena':
    return {start: "[tex:", end: "]"};
    break;
  }
}

考えた手順

それぞれの文法には以下のルールがあります。
【数式開始の文言】
Qiitaの文法 ```math
Kibelaの文法 ~~~{latex}
はてなブログの文法 [tex:

【数式終了の文言】
Qiitaの文法 ```
Kibelaの文法 ~~~
はてなブログの文法 ]

考え方としては、例えば、Qiitaのフォーマットで書いたテキストをKibelaフォーマットに変換したい場合、入力したテキストの中に```mathという文字列があれば、~~~{latex}へ変換。```という文字列があれば~~~へ変換する、という考え方です。

では、入力したテキスト中の全ての```math~~~{latex}に置換して、そして同じように```~~~へ全置換しよう。JavaScriptのreplaceを使えば一発OK!

そう考えてしまうかもしれませんが、このやり方だと以下のようなテキストの場合うまくいきません。



   ```javascript
   fuga
   ```

   ```math
   hogehoge
   ```
   text text text text.
   ```math
   hogehoge
   ```


例えば、このようなテキストの場合、全ての```mathを置換してしまったあと、残りの```を置換しようとしても、どの``````mathとセットになっているものかわからないため、置換する必要がないjavascriptの終始記号をも置換してしまいます。

そのため、置換を実行する時は、```mathを置換し、そのあとに現れる```~~~に置換するという実装にする必要があります。
Qiita→Kibelaであれば上の実装ですが、Qiita→はてなブログ、Kibela→Qiita、Kibela→はてなブログ。
どの変換も置換する文字列が違うだけで、あとの実装の流れは一緒です。

課題

今回の実装では、文中に数式を埋め込む文字列の変換が実装できていません。
そしてはてなブログ→{Qiita, Kibera}の変換がまだ実装できていないのでぜひやり方をおしえて欲しいです。。

★数式変換ツールのレポジトリはこちら★

また、このような変換サービスが既存であるよなどの有力情報があればぜひ教えてほしいです。

おわり

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む