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

【React入門】学習メモ #2

この記事はReact Advent Calendar 2020 17日目の記事です。

前回の記事はこちら

はじめに

React学習の備忘録です。
間違い等ございましたら、ご指摘いただけますと幸いです

ブラウザ表示の流れ

ファイルは下記の順番に変換されます。
App.jsに記述されているJSXは最終的にHTMLに変換されてブラウザに表示されます。

  1. App.js
  2. index.js
  3. index.html
  4. ブラウザ(chrome, firefox..)

コンポーネント

コンポーネントとは、「部品」「パーツ」といった意味で、
Reactでは、コンポーネントを組み合わせてUIを構築します。

コンポーネントの定義方法

  • クラスコンポーネント(class)
  • 関数コンポーネント(function)

クラスコンポーネントの構成

Sample.jsx
import React from 'react';  //reactをimport

class Sample extends React.Component {  //React.Componentを継承
  render() {
    return(
      {/* JSX */}
    );
  }
}

関数コンポーネントの構成

Sample.jsx
import React from 'react';  //reactをimport

const Sample = (props) => {
  return(
    {/* JSX */}    
  )
};

コンポーネントがブラウザに表示されるまで

コンポーネントをApp.jsで呼び出して、表示させることで最終的にブラウザに表示されます。
流れは以下です。

  1. Sample.jsx
  2. App.js
  3. index.js
  4. index.html
  5. ブラウザ(chrome, firefox..)

コンポーネントを表示

コンポーネントをApp.jsで呼び出すためには、コンポーネントをexportする必要があります。
下記はクラスコンポーネントの例です。

Sample.jsx
import React from 'react';
class Sample extends React.Component {
  render() {
    return(
      {/* JSX */}
    );
  }
}
export default Sample;  //Sampleコンポーネントをexportする

App.jsでは、
1. 呼び出すコンポーネントをimportして読み込む
2. JSX内でコンポーネントを記述する

App.js
import React from 'react';
import Sample from './Sample';  //【1】Sampleコンポーネントをimportする
class App extends React.Component {
  render() {
    return(
      <Sample />  {/* 【2】Sampleコンポーネントを読み込む */}
    );
  }
}

props

propsとは、コンポーネントから渡される引数的なものです。

以下、引用

  • immutable data(不変のデータ)
  • passed in from parent(親から渡される)
  • can't change it(変更不可)
  • can be defaulted & validated (デフォルト値の設定と検証が可能)

propsの渡し方

「props(プロパティ)名 = 値」という形式で渡す

App.js
render() {
  return(
    <Human
      name = 'masa'  {/* プロパティ: name, 値: 'masa' */}
      age = 21  {/* プロパティ: age, 値: '21' */}
    />
  );
}

propsの取得

this.propsで取得できます。

Human.jsx
render() {
  return(
    <div>
      <div className="human-name">
        { this.props.name }  {/* nameプロパティを取得 */}
      </div>
      <div className="human-age">
        { this.props.age }  {/* ageプロパティを取得 */}
      </div>
    </div>
  );
}

mapメソッドの使い方

コンポーネントの数が多くなるとその分、コンポーネントもpropsも記述しないといけなくなり、コードが肥大化してしまします。そのようなケースにmapメソッドを使用します。

mapメソッドを使用することによって、以下のメリットがあります。

  • 冗長なコードを記述せずに済む(肥大化防止)
  • propsのプロパティと値を配列にまとめることで管理が楽になる
  • コンポーネントを何個も記述せずに済む
App.js
class App extends React.Component {
  render() {
    // 配列
    const humanList = [
      { name: 'masa', age: 21 },
      { name: 'tomoaki', age: 24 },
      { name: 'naoki', age: 15 },
      { name: 'takahiro', age: 51 },
      { name: 'haru', age: 37 }
    ]
    return(
      <div>
        {humanList.map((human) => {
          return(
            <Human
              name = { human.name }
              age = { human.age }
            />
          )
        })}
      </div>
    );
  }
}

mapメソッドでは、配列の格propsがhumanList.mapの引数(human)に格納されてます。
それからコンポーネント側で、{ human.name }や{ human.age }でpropsを渡しています。

備考

良ければ、続きの記事も見て頂けますと幸いです。

【React入門】学習メモ #3

参考記事

Reactにおけるstateとpropsの違い

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

JSONについてまとめ

JSONについて調べたことをまとめました。

JSONとは

JavaScript Object Notaitonの略語

データを表示するための表記法の一種

他にXMLymlなどがある

JSONの特徴

扱えるデータの種類

  • 文字列
  • 数値
  • オブジェクト
  • 配列
  • 真偽値
  • null

jsonのデータ表記例

山田太郎さんのデータの例
{
  "first-name": "太郎",
  "last-name": "山田",
  "age": 25,
  "height": 170,
  "weight": 65 
}

その他

  • JavaScriptのオブジェクトの書き方と似ているが、別物
  • キー、文字列はをダブルクォーテーションで囲む必要がある(シングルはNG)

JSONの用途

  • API通信時
    • ReqestResponseのデータに使用
    • 以前まではXMLを使用していた
  • DBの形式
    • firebaseMongoDB等のデータ保管
  • package.json
    • Node.jsパッケージのメタデータ保管
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flash Advent Calendar 14日目 - WebSocketのハンドシェイクをエミュレートする -

FlashのSocketクラスの実装の時にすごくハマったので記事にしようと思います。
通常のJavaScriptではそもそも作る必要のない機能なので、笑い話としてみてもらえればと思います。

通常のWebSocketの流れ

const socket = new WebSocket("ws://example.com/chat");

// 通信が確立した時のイベント
socket.onopen = function (event) 
{
    alert("[open] Connection.");
};

// メッセージを受け取った時
socket.onmessage = function (event) 
{
    alert(`[message]: ${event.data}`);
};

はい。これで完了です。
凄く簡単に始めれます。

が、FlashのSocketクラスはブラウザがない事が前提なので
本来であればブラウザがやってくれるあれやこれやを自前でやる必要があります。

まず、Socket通信を確立する為に、ハンドシェイクを行う必要があります。

参考サイト
スクリーンショット 2020-12-16 23.23.18.png

サーバーに開始する為のリクエストを送ります。

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

HTTPバージョンは1.1以上で、メソッドはGETでなければだめです。
次に大切なのがbase64化されたSec-WebSocket-Key
このキーを元にリクエストに対応するレスポンスであることを確認します。

メッセージを送る度にヘッダーを生成して送ります。
ヘッダーの最後は改行が2個必須なのも注意点です。
(イメージしやすくする為に改行コードを追記してます。)

HTTP/1.1 101 Switching Protocols\n
Upgrade: websocket\n
Connection: Upgrade\n
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=\n
\n

Sec-WebSocket-Acceptの生成方法は
Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 (<=固定値)
をSHA1でhash化して最後にbase64化します。

まじか・・・

っという事で、自前でSHA1のハッシュ化するロジックを作成します。
ActionScriptのクラスを参考にしながら作っていきます。

/**
 * @param  {string} value
 * @return {string}
 * @public
 */
toBase64 = function (value)
{
    const buffer    = createBlocksFromString(value);
    const byteArray = hashBlocks(buffer);

    let hash = "";
    const length = byteArray.length;
    for (let idx = 0; idx < length; ++idx) {
        hash += String.fromCharCode(byteArray.readUnsignedByte());
    }

    return window.btoa(hash);
}

/**
 * @param  {string} value
 * @return 
 * @public
 */
createBlocksFromString = function (value)
{
    const blocks = [];

    const length = value.length * 8;
    for (let idx = 0; idx < length; idx += 8) {
        blocks[idx >> 5] |= (value.charCodeAt(idx / 8) & 255) << (24 - idx % 32);
    }

    // append padding and length
    blocks[length >> 5] |= 0x80 << (24 - length % 32);
    blocks[(((length + 64) >> 9) << 4) + 15] = length;

    return blocks;
}

const int32Array = new Int32Array(1);
/**
 * @param {number} value
 * @return {int}
 * @public
 */
toInt32 = function (value)
{
    int32Array[0] = value;
    return int32Array[0];
}

/**
 * @param  {array} blocks
 * @return {ByteArray}
 * @public
 */
hashBlocks = function (blocks)
{
    let a = 0;
    let b = 0;
    let c = 0;
    let d = 0;
    let e = 0;

    let h0 = 1732584193;
    let h1 = 4023233417;
    let h2 = 2562383102;
    let h3 = 271733878;
    let h4 = 3285377520;

    const tmp = new Util.$Array(80);
    let pos   = 0;
    let index = 0;

    const length = blocks.length;
    for (let idx = 0; idx < length; idx += 16) {

        a = h0;
        b = h1;
        c = toInt32(h2);
        d = h3;
        e = h4;

        pos = 0;
        for (; pos < 20; ++pos) {

            if (pos < 16) {

                tmp[pos] = blocks[idx + pos];

            } else {

                index = tmp[pos - 3] ^ tmp[pos - 8] ^ tmp[pos - 14] ^ tmp[pos - 16];
                tmp[pos] = index << 1 | index >>> 31;

            }

            index = toInt32((a << 5 | a >>> 27) + (b & c | ~b & d) + e + toInt32(tmp[pos]) + 1518500249);

            e = d;
            d = c;
            c = b << 30 | b >>> 2;
            b = a;
            a = index;
        }

        for (; pos < 40; ++pos) {

            index = tmp[pos - 3] ^ tmp[pos - 8] ^ tmp[pos - 14] ^ tmp[pos - 16];
            tmp[pos] = index << 1 | index >>> 31;
            index = toInt32((a << 5 | a >>> 27) + (b ^ c ^ d) + e + toInt32(tmp[pos]) + 1859775393);

            e = d;
            d = c;
            c = b << 30 | b >>> 2;
            b = a;
            a = index;
        }

        for (; pos < 60; ++pos) {

            index = tmp[pos - 3] ^ tmp[pos - 8] ^ tmp[pos - 14] ^ tmp[pos - 16];
            tmp[pos] = index << 1 | index >>> 31;
            index = toInt32((a << 5 | a >>> 27) + (b & c | b & d | c & d) + e + toInt32(tmp[pos]) + 2400959708);

            e = d;
            d = c;
            c = b << 30 | b >>> 2;
            b = a;
            a = index;
        }

        for (; pos < 80; ++pos) {

            index = tmp[pos - 3] ^ tmp[pos - 8] ^ tmp[pos - 14] ^ tmp[pos - 16];
            tmp[pos] = index << 1 | index >>> 31;
            index = toInt32((a << 5 | a >>> 27) + (b ^ c ^ d) + e + toInt32(tmp[pos]) + 3395469782);

            e = d;
            d = c;
            c = b << 30 | b >>> 2;
            b = a;
            a = index;
        }

        h0 += a;
        h1 += b;
        h2 += c;
        h3 += d;
        h4 += e;

    }

    // ByteArrayクラスも自前作る必要があるのですが、ここはまたいつか・・・
    const byteArray = new ByteArray();
    byteArray.writeInt(h0);
    byteArray.writeInt(h1);
    byteArray.writeInt(h2);
    byteArray.writeInt(h3);
    byteArray.writeInt(h4);
    byteArray.position = 0;

    return byteArray;
}

ここまでできて、初めて通信のやとりとができる状態となります。

少し長くなってしまったので、今日はここまでにします。

明日は、ブラウザが行っているメッセージの暗号化をどうやって作るかを書こうかと思います。

本来はブラウザが全部やってくれてる事なので、本当に感謝です。。。

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

Mapbox GL JS 地図上にユーザーの位置情報履歴を、ラインで表示する

地図に位置情報の履歴が、軌跡として表示されている様子

こんにちわ。
@isnot_naoto (GitHub/isnot) です。

Mapbox Advent Calendar 2020の、16日目の記事を投稿します。

私はMapbox GL JSが気に入っており、このアドベントカレンダーのことを知って、記事を投稿しようと思いました。
今回の記事は、私がこの夏に作成した地図※から、実際に使っているテクニックを切り出して、サンプルに再構成したものになります。

特に断りがない限り、実装はES2018準拠を基本としています。

Class userLocationHistory

位置情報の履歴を保持するためのクラスを作ります。

userLocationHistory コード
class userLocationHistory {
  constructor() {
    this.history = new Set();
    this.last_location = undefined;
    this.min_duration = 2;
    this.max_history = 150;
    this.time_exceed = 3600;
  }

  _hasProperty(obj, prop) {
    return Object.prototype.hasOwnProperty.call(obj, prop);
  }

  _add(geolocate) {
    if (this._hasProperty(geolocate, 'timestamp')) {
      this.history.add(geolocate);
      this.last_location = geolocate;
    }
  }

  size() {
    return this.history.size;
  }

  getLast(size = 1) {
    const asize = this.size < size ? this.size : parseInt(size, 10);
    if (asize === 1) {
      return this.last_location;
    }
    return Array.from(this.history).slice(-1 * asize);
  }

  reduce(size) {
    if (this.size < 2) {
      return;
    }
    if (size === 0) {
      this.history.clear();
    } else {
      this.history = new Set(this.getLast(size));
    }
    return;
  }

  elapseTimeInSeconds(geolocate) {
    if (this.last_location === undefined) {
      return 0;
    }
    if (!this._hasProperty(geolocate, 'timestamp')) {
      return 0;
    }
    return parseInt((geolocate.timestamp - this.last_location.timestamp) / 1000, 10);
  }

  addGeolocate(geolocate) {
    // console.debug(geolocate, this.history);
    const elapse = this.elapseTimeInSeconds(geolocate);

    if (this.last_location === undefined) {
      this._add(geolocate);
    } else if (elapse > this.min_duration) {
      this._add(geolocate);
    }
    const new_size = elapse > this.time_exceed ? 0 : this.max_history;
    this.reduce(new_size);
  }

  getUserTrack() {
    const t = [];
    for (const item of this.getLast(this.max_history)) {
      t.push([item.coords.longitude, item.coords.latitude]);
    }
    return t;
  }
}

コンストラクタの中で、いくつかの定数を決めています。その意味は以下の通りです。
より実践的な実装とするためには、このような数値を外部から設定できるような作りにすると良いかと思います。

  • min_duration 位置情報を保持する最小の間隔 (2 [seconds])
  • max_history 位置情報を保持する最大の件数 (150 [count])
  • time_exceed 位置情報を保持する期限 (3600 [seconds])

メソッド

addGeolocate(geolocate)
geolocateで渡した位置情報を、履歴に追加する
getUserTrack()
保持している履歴を、[ [lon, lat], ... ] で返す
size()
保持している履歴の件数を返す
getLast(size = 1)
保持している履歴を、新しい方から順に、size件を返す
reduce(size)
新しい方からsize件を残して、古い履歴を破棄する
elapseTimeInSeconds(geolocate)
geolocateで渡した位置情報について、保持している最後の履歴からの経過時間を秒単位で返す
_add(geolocate)
位置情報の履歴を新しく追加する (private)
_hasProperty(obj, prop)
組み込みの hasOwnProperty へのショートカットです (private)

userLocationHistory 工夫したところ

履歴に保持する位置情報について、それぞれの隣接するアイテムの間隔が、あまり細かくなりすぎないように、最小の秒数を決めて、抑制するようにしました。
また、件数についても、最大値となる件数を決めて、それより多くならないようにしています。
さらに、一定時間を経過した履歴は、破棄するようにしました。

位置情報をたくさん溜め込み過ぎると、それらを地図内に描画する際に、ゴチャゴチャとし過ぎるかなと思ったのと、「重くなる」のを防ぐために、上記のような工夫を入れています。

地図内に、geojson型式のラインを描画する

参考:Add a GeoJSON line

function setupGeoLine() {
  map.addSource('route', {
    'type': 'geojson',
    'data': {
      'type': 'Feature',
      'properties': {},
      'geometry': {
        'type': 'LineString',
        'coordinates': []
      }
    }
  });
  map.addLayer({
    'id': 'route',
    'type': 'line',
    'source': 'route',
    'layout': {
      'line-join': 'round',
      'line-cap': 'round'
    },
    'paint': {
      'line-color': '#888',
      'line-width': 4
    }
  });
}

function updateGeoLine(coordinates) {
  map.getSource('route').setData({
    'type': 'Feature',
    'properties': {},
    'geometry': {
      'type': 'LineString',
      'coordinates': coordinates
    }
  });
}

map.on('load', () => {
  setupGeoLine();
});

最初に地図を読み込んだ際に、setupGeoLine() でLayerを追加しておきます。
最初は、描画するコンテンツは無くて、画面上の変化はありません。

その後適宜に updateGeoLine() を実行すると、渡した位置情報の配列を使い、グレーのラインを描画します。
単純に、連続した座標同士を繋ぐ(辿る)ような線になります。

GeolocateControl を使って、位置情報を所得する

参考:Locate the user

const userTrack = new userLocationHistory();

function onGeolocate(pos) {
  userTrack.addGeolocate(pos);
  updateGeoLine(userTrack.getUserTrack());
}

const geolocate = new mapboxgl.GeolocateControl({
  positionOptions: {
    enableHighAccuracy: true
  },
  trackUserLocation: true
});
map.addControl(geolocate);
geolocate.on('geolocate', onGeolocate);

ユーザーの位置情報を所得するには、Mapbox GL JSの、GeolocateControl を使います。
map.addControl() することで、地図内にコントロール(ボタン)が追加されます。
地図右上の、灯台の地図記号のような見た目のボタンです。

GeolocateControlのボタン

初期状態では位置情報にアクセスしません。
ボタンを押すと、ユーザーの許可を得て、位置情報が取得できる状態になります。

具体的には、位置情報が変化する度に、geolocate イベントが発生するので、それをフックするようにコールバックを登録しておくことで、位置情報(緯度、経度、タイムスタンプ)を得ます。

地図を初期化

参考:Display a map, Use locally generated ideographs

const home_loc = {
  center: [139.7454511, 35.6585648],
  zoom: 17,
  pitch: 60,
  bearing: 0
};

mapboxgl.accessToken = 'pk.your_accesstoken_here';
const map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/mapbox/streets-v11',
  center: home_loc.center,
  zoom: home_loc.zoom,
  pitch: home_loc.pitch,
  minZoom: 4,
  hash: true,
  localIdeographFontFamily: "'Noto Sans CJK JP', 'Noto Sans', sans-serif"
});

Mapbox GL JSではお馴染みの部分です。
「localIdeographFontFamily」というのは、フォントデータをサーバーからダウンロードするかわりに、ローカル(ブラウザ)のフォントを使ってレンダリングするようになるというもののようです。これにより、地図を最初にロードする時間が、速くなることを期待しています。

HTMLとスタイルシート
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v2.0.0/mapbox-gl.css" />
<style>
  #map { height: 600px; }
</style>
<div id="map"></div>
<script src="https://api.mapbox.com/mapbox-gl-js/v2.0.0/mapbox-gl.js"></script>

おわりに

FullSource (GitHub)

このサンプルは、技術的なデモンストレーションとして、また、一部もしくは全ての部分を、自由に再利用されることを意図して公開しております。
詳しくはREADME.mdLICENSE(MIT License)をご覧ください。

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

【個人アプリ作成】情報管理アプリ制作16日目

昨日は仕事に100%割いたので悔いなし。今日の実績。

今日やったこと

・ 顧客データ登録画面の作成
・ 中間テーブルの保存の復習
・ JavaScriptで装飾(ボタンをクリックするとメニューが出る)

中間テーブルの保存

createアクションに対して、{:users_id []}の記述で自動で連携・保存される(ストロングパラメーターは必須だけど)

アソシエーションを組んでいるためのなのか、保存される仕組みは完璧には分からないかったが、仕組みは理解。明日から実装していく。

JavaScriptが全然うまくいかない

Id属性の情報は取れているのに、Chromeでデバックするとすぐリロードされて消える、、、、

function clickbutton(){
  const pushPlusButton = document.getElementById("index-plus")

  pushPlusButton.addEventListener('mouseover', function(){
    console.log("aaaa")
  })
}

window.addEventListener('load', clickbutton)

console.logの記述が一瞬だけ出てすぐ消える、、、
turbolinksの記述は消した方がいいはずなんだけど、ダメだったのか、、、

実装苦戦中です。。。。

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

Flash Advent Calendar 13日目 - テキスト入力の再現方法 -

Flashでは任意の箇所にテキスト入力ができます。
ただ、Canvasは画像なので、テキスト入力には対応していません。
なので、HTMLのTextAreaを駆使して再現する必要があります。

方法を記載すれば、なんだ、そんな事かとなるのですが
派手な実装ではないのと意外と管理が面倒なので、後回しにされがちです。

目次

  • テキストエリアにヒットした時にマウスのカーソルを変化させる
  • 発火するイベントは全て移植する

テキストエリアにヒットした時にマウスのカーソルを変化させる

スマホでは見れないのですが、PCだとここが記入エリアだよって事が分かりやすので
意外と必要な機能でした。

  1. MOUSE_MOVEで座標情報取得して、エリア内の座標が入ったらカーソルを変えて
  2. MOUSE_DOWNでTextAreaをCanvasの上に透過して設置して文字の入力を行えるようにする
  3. エリアから外れると、TextAreaを削除してカーソルを元に戻します。

2020-12-16-224830.gif

発火するイベントは全て移植する

FlashでTextFieldクラスに定義されてるイベントは全て移植してcanvasに反映する
文字制限などあれば入力のイベントの度に文字データを受け取り処理を行い、またTextAreaに結果を反映していく

例)入力制限a-zのみ入力可能

textField.restrict = "a-z";

2020-12-16-230014.gif

便利な機能なのですが、意外と日の目をみない機能なので
せめてここで披露したいと思い記事にしました。

明日はWebSocketのハンドシェイクをエミュレートした話を書こうと思います。

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

プログラムを少しかじった私がバックエンドエンジニアとして働くためにECサイトを作り始めてみた その②

対象者

・バックエンドエンジニアを志す人
・プログラミング開発初学者

はじめに

こんにちは!

この投稿はその①の続きです。
その①

phpはコードを書いていて好きになりました。
好きになった理由を考えてみましたが下記があげられました。

・コードだけで機能を実装できるところ
・構造を考える楽しさ(データベース)
・機能(仕組み)を知る楽しさ

なるほど、好きになった理由はわかりました・・。
好きになったものをきっかけに働いていこうと思ったのが私です。

では、働くとしたらどんな働き方になるだろう
そんな時に出てきた言葉がバックエンジニアという言葉でした。
あー聞いたことがあるバックエンドエンジニアかPHPとかAWSとかね。

ってほとんど何も知らん。笑

なので調べていきます。

目次

1.バックエンドエンジニアとは?
2.抽象度が高いのでもっと具体的にする。
3.【結論】プロダクトを作ることがバックエンドエンジニアになる1番の近道
4.次のステップ:ECサイト作成

1.バックエンドエンジニアとは?

バックエンドエンジニアについて書かれたページを確認しました。

Aサイト:バックエンドエンジニアとは?

バックエンドは、サーバーサイド(Webサーバー側)やデータベースのシステムなど、ユーザーの目に見えない部分のことです。

ほう、ユーザに見えない部分ね・・
感想:抽象度が高すぎてわからん。見えない部分ってなにがあんねん。

2.抽象度が高いのでもっと具体的にする。

いや、確かに調べてみたらいっぱいあるんですよ・・
でも、細かな話するといっぱいありすぎるわけで。

軽く調べただけでも決済機能や認証機能、webサーバ、ドメインなど色んな言葉がありました。バックエンドエンジニア目指したいのにてんこ盛りの言葉の前にどうすればいいのかと思っちゃいました。

3.【結論】プロダクトを作ることがバックエンドエンジニアになる1番の近道

バックエンドエンジニアは結局な所、プロダクトを作るための技術を行う人(ま、エンジニアのほとんどがそうだと思ってる)だと思うからプロダクトを実際につくることがバックエンドエンジニアが使っている技術を身につけることに近づくという結論にいたりました。

ま、プロダクトと一言で言っても使われている技術はもちろんバックエンドだけとは限らないので作るプロダクトはバックエンドを最大限に活かしたプロダクトにしていきたいと考えています。

4.次のステップ:ECサイト作成

最初のプロダクトはECサイトにします。
どんな技術が使われているかわからないのでまずは調べたいと思います。
調べた内容はまた記事にします。

関連記事一覧
その①

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

GmailのAPIでのエラー: Metadata scope doesn't allow format FULL

users.messages.getのAPIを使おうとしてのエラーメモです。

https://developers.google.com/gmail/api/reference/rest/v1/users.messages/get

Metadata scope doesn't allow format FULL

というエラーが出ました。

ドキュメントにこれらのパーミッションが必要と書いてあるので許可してたのですが......

https://mail.google.com/
https://www.googleapis.com/auth/gmail.modify
https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/gmail.metadata
https://www.googleapis.com/auth/gmail.addons.current.message.metadata
https://www.googleapis.com/auth/gmail.addons.current.message.readonly
https://www.googleapis.com/auth/gmail.addons.current.message.action

検証環境

  • Node.js v15.3.0
  • googleapiモジュール v66

試したコード

//省略

gmail.users.messages.get({
    userId: 'me',
    id: `xxxx`,
    // format: 'FULL' 
}, (err, res) => {
   if (err) return console.log('The API returned an error: ' + err);

   console.log(res);
});

//省略

ちなみにallow format FULLとなっていたのは、何も指定しないとデフォルトのフォーマットがFULLになるからという理由です。

  • MINIMAL
  • FULL (デフォ)
  • RAW
  • METADATA

https://developers.google.com/gmail/api/reference/rest/v1/Format

/auth/gmail.metadataのパーミッションを許可しない

Metadata scope doesn't allow format FULL gmailとのことなのでそのままなのですが、APIドキュメントのテスターで試そうとしたら、/auth/gmail.metadataのパーミッションがチェック外れている状態でした。

スクリーンショット 2020-12-16 21.38.10.png

試しにチェックして試すと、テスター上も同様のエラーが発生。

ということでトークンを作成する際に/auth/gmail.metadataを許可しなければOKです。

トークン作成する際のスコープから/auth/gmail.metadataをはずしてトークンを作成しなおしたらいけました。

const SCOPES = [
    `https://mail.google.com/`,
    `https://www.googleapis.com/auth/gmail.modify`,
    `https://www.googleapis.com/auth/gmail.readonly`,
    // `https://www.googleapis.com/auth/gmail.metadata`,
    `https://www.googleapis.com/auth/gmail.addons.current.message.metadata`,
    `https://www.googleapis.com/auth/gmail.addons.current.message.readonly`,
    `https://www.googleapis.com/auth/gmail.addons.current.message.action`
];

まとめ

これはドキュメントミスな気がするけどどうなんでしょう。。

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

インストール後のパス設定。みんなどうしてる?

みなさん、こんにちは。本日のお悩み相談の時間です。

前書き

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でおなじみのスクリプト言語 Kinx。詳しくは Web(下記)で。

実は最近 バージョン 0.17.0 を(プレ)リリース しまして、そこで初めてインストーラを付けてみました。NSIS を使ってビルドしています。

ところが、今現在はインストール後に実行ファイルへのパスを環境変数 Path に追加 いたしません。手動でパスを通してください的な。

元々環境変数 Path をイジるとか おっかないなー、というのもあったのですが、どうも NSIS で実現しようとすると問題がありそうなのですね(この辺とか)。ただ、やっぱりパスを自分で通せ、というのは心苦しいので次版では付けたいところ。

そこで、皆さんはインストーラーで環境変数 Path へのパスの追加/削除はどうしてますか?というのが本日のクエスチョンです。

色々調べたのですが、あんまりいいアイデアが見つからずで。一応、以下のような基準で探してました。

  • シンプルなソースコード
  • 使いやすい機能
  • 便利なライセンス

全部満たすものはなかなか見つからず。ライセンスが書いてないものとか、GPL とか1

結論

そこでだ、若旦那!

簡単なプログラムを作成しました。こちらをご覧ください。

これは...

  • シンプルなソースコード... 1つのファイル だけで単機能ツールとして実現。
  • 使いやすい機能... 単機能ツールなので 使い方も単純
  • 便利なライセンス... ザ・MIT!(私がそう設定したのですが)

ですが...

このプログラムはシステム環境を変更するので、たくさんの人にソースコードをチェック してもらいたく、問題があれば修正 したいなーと思います。一応簡単なテストコードは含めてチェックはしてますが。

ということで、ソースコードを色々な人に見て貰えると嬉しいです。問題見つかれば大変感謝するでしょう(それ以上のことは力不足で何もして差し上げられませんが)。

もしくは、この問題(パスの追加/削除)に対するより良い解決策があれば教えてください。

ではまた。

P.S.
もし、これ (https://github.com/Kray-G/addpath) 自体気に入ってくれるようでしたら、★ください。待ってます!


  1. 念のため、GPL が悪いものとは言ってませんので...。自分のが MIT なので採用しづらいなと。 

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

JavaScript,8,条件分岐,if,else,初心者向け

8,条件分岐 if else

JS(JavaScript)で条件分岐をおこなってみましょう!

const score = 20;

if (score === 20)   score と20が一緒
if (score < 20)      score より20が大きいか
if (score > 20)      score より20が小さいか

const score = 10;

if (score === 20) {
  document.getElementById("test").textContent = "スコアは20です"
} else if (score === 10) {
  document.getElementById("test").textContent = "スコアは10です"
} else {
  document.getElementById("test").textContent = "スコアは20ではないです"
}

もしこれなら次こうする
けど違ったらこれするもしくはこれか
違ったらこれする

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

Ansibleのつらみ - 俺ならこう実装するのに!

昨年からだいぶサボっていましたが、個人的にAnsibleに足りないと感じている機能を実装した、オレオレ構成管理ツールであるSubmarine.jsというツールを開発しているので、Ansibleとの比較で紹介していこうと思います

リポジトリ: https://gitlab.com/mjusui/submarine2

Ansibleにこんな機能あったらいいのにという願望ととらえて読んでいただければと思います

Python環境構築のハマりどころ

Ansibleをインストールするのに、まず大事なのはPython環境を整えることです。その際の注意点として以下のようなものがあります

  • Python, pip, Ansibleそれぞれのバージョン管理が必要
    • Python2系と3系で互換性がない
    • pip install するときはバージョンを指定しよう
    • 同じAnsible2.x系でも、後方互換性がない機能がある
    • pyenv/virtualenvがおすすめ

一方、Submarine.jsでは、Node.jsのバージョン以外には極力依存しないように実装されています

  • Node.jsのバージョンのみに依存
    • Node.jsは、ブラウザで動作するJavaScriptの派生なだけあって比較的、後方互換性がある
    • パッケージはいっさい使わないので、依存関係の解消は不要
    • 動作が安定してきたら、Submarine.js自体の後方互換性も長めに維持していく予定(願望)
    • nvm環境(Pythonで言うところのpyenvに相当)がおすすめ

Dynamic Inventoryは、もっと簡単にできる

Dynamic InventoryはPlaybookを実行する対象のインベントリを、動的に生成する機能ですが、以下の点でとっつきにくさがあります

  • Dynamic Inventoryの特徴
    • スクリプトで複雑なJSONを生成する必要がある
    • 生成されたインベントリの一覧を、手軽に確認できない

これに対してSubmarine.jsでは、generatorとfilterという2つの概念を組み合わせることで、簡単に実現できるようになっています

collect.js
module.exports={
  collect: [{
    type: 'gen/bash',
    cmd: 'echo 172.17.0.{1..254}',
  }, {
    type: 'fil/ping',
  }],
};

このコードは172.17.0.1から254のIPアドレスに対してpingを打って、応答があったホストだけ抽出するSubmarine.jsのコードです

Node.jsのコードを書く必要があるのですが、構成管理に関わる部分は、ほとんどJSONが書ければ問題ありません

type: 'gen/bash' というのは、Bashでコマンド実行した結果をもとにターゲットホストのリストを生成するgeneratorです
そして type: 'fil/ping' というのはgeneratorで生成したリストにpingを打って、応答結果に応じてリストを絞り込むfilterです

このようにSubmarine.jsでは、シンプルなJSONで、AnsibleのDynamic Inventoryに相当する機能が実現できます

冪等性を担保するのは、ユーザの仕事

Ansibleの重要な概念の一つとして、冪等性というものがあります

Ansibleでは、これを担保するために膨大な数のモジュール群が提供されていますが、それらがバージョンごとに微妙に挙動が変わったりして、メンテナンスが大変だったりします

また、複雑なことをしたい場合は、結局自分で冪等なスクリプトを書く必要があり、それもなかなか骨の折れる作業です

Submarine.jsでは、冪等なモジュールを提供するのではなく、Dockerfileに学び、イミュータブルなShellScriptを書くというアプローチを取っています

1度ShellScriptを実行すると、サーバ側にロックファイルを生成し、2回目以降はロックファイルがある場合は、実行をスキップするように実装されています

コードはこんな感じ

provisoin.js
module.exports={
  provision: {
    gen: 'mysql-server-1',
    opts: ['-l submarine'],
    cmds: [{
      name: 'install-mysql',
      cmd: String.raw`
        cd /usr/local/src/mysql \
        && ./configure \
        && make install
      `,
    }, {
      name: 'install-curl',
      cmd: 'apt install -y curl',
    }]
  },
};

cmds という配列の中に、実行するコマンド(cmd)と、ロックファイルの名前として name を指定します

ちなみに1度実行したスクリプトを修正したい場合は、既存の cmd を変更するのではなく、cmds の末尾に、新しく処理を追加するようにします
パッチを当てるような感覚ですね

こうすれば冪等性に配慮してツール固有のモジュールの使い方を学んだり、if文を書いたりする必要はありません

インフラの構成管理は、プログラミングよりDatabase設計に近い

インフラの構成をコードとして管理することをIaC(Infrastructure as Code)と言いますが、実際インフラの構成管理とプログラミングは一緒くたに扱うことができない異なる特徴があります

まずインフラの世界では、ミドルウェアごとにインストールのしかたや、設定ファイルの書き方が異なり、プログラミングのように十分に整備されたSyntaxというものがありません

またプログラミングの主たる目的は、メモリやDatabase上のデータを参照/変更することです。一方インフラの場合、例えばミドルウェアのインストールなどは、それよりも処理コストがかかる上、一度インストールしたソフトウエアのバージョンは、簡単には変更できません(この点コンテナ技術は優れていると言えるでしょう)

インフラの世界では、プログラミングよりもコストがかかる、可逆性の低い状態遷移があるのです
そのためバグが発見されたら、古いタグでリリースし直せば解決、とはいかないことも多いです

Ansibleでは、サーバAとBで同じ構築手順を共有していた場合、roleという単位でひとまとめにすることはよくありますが、これをすると、例えばサーバAの構築手順を変更する場合、この共有しているコードに手を加えるとサーバBにも影響が出てしまうので、メンテナンスしづらいということはよくあります

プログラミングの世界ではDRYということが、しばしば美徳とされますが、IaCの世界では必ずしもそうではないのです

この点Submarine.jsでは、構築手順を共有しているサーバが複数あったとしても、構成ファイルは別々に分けることを推奨することにしています
実際、構成情報は1つのファイルにまとめて書くようになっているので、コードの共有がしづらいようになっています

インフラの世代管理に弱いAnsible

インフラはプログラミングとは違い、構成変更の敷居が高いことが分かりました。そのこともあり、インフラの世界では、本番環境に複数世代のインフラが並行稼働するようなこともあります

そしてAnsibleは、この複数世代の構成管理が、あまり得意で無いように感じます。roleを使ってDRYを実現することで、特定のサーバだけ構成変更するようなメンテナンスがしづらくなることは、さきにも述べました

このあたりのことは昨年の私の記事にも、少し書きました

Submarine.jsでは、構成情報に「世代」の概念を導入しています

provision.js
module.exports={
  provision: {
    gen: 'mysql-server-1',
    opts: ['-l submarine'],
    cmds: [{
      name: 'install-mysql',
      cmd: String.raw`
        cd /usr/local/src/mysql \
        && ./configure \
        && make install
      `,
    }, {
      name: 'install-curl',
      cmd: 'apt install -y curl',
    }]
  },
};

これは先ほどの、イミュータブルなShellScriptの説明でも出てきたコードです
ここで gen: 'submarien-test-2' と書かれた部分が世代にあたります

Submarine.jsは、はじめにスクリプトを実行するときに、サーバ側にファイルを生成し、この世代の情報を保持しておいて、2回目以降は、この世代情報が一致しないサーバには、スクリプトを実行しないような挙動をします

つまり、ある世代のサーバに、別の世代のコードが実行されないように保護してくれるのです

これは例えば、あるサーバのグループのうち、試しに一部のサーバだけをリプレース(ローリングアップデート)したいときなどに役に立ちます。OSやミドルウェアアップデートをするときには当然、インストール手順も変わるので、provisionの定義も変更する必要があります。そこで変更した定義を構築済みの旧世代のサーバには実行せず、新しくOSをインストールした新世代のサーバにのみ実行することができるのです

サーバを構築したら、正しくできたか確認しよう

構成管理・自動化ツールであるAnsibleとは、少し離れますがSubmarine.jsには、構築したサーバをテストする機能もあります

サーバのテストというと、一般的にはAnsibleエコシステムの1つであるTestInfraや、Ruby系のツールであるServerspecが上げられますが、これらのツールでは、サーバから値を取得するためにモジュールが提供されています

一方Submarine.jsでは、値の収集もShellScriptで行います

query-and-test.js
module.exports={
  query: {
    opts: ['-l submarine'],
    query: {
      hostname: 'hostname -s',
      submarine_user: 'cat /etc/passwd|grep submarine|wc -l'
    }
  },

  test: {
    func: (host, all)=>{
      return {
        host_count: Object.keys(all).length === 10,
        hostname: host.hostname === 'ubu2004-submarine-target',
        submarine_user: host.submarine_user === '1',
      };
  },
};

上記の query という部分で、テストしたい値を収集するShellScriptを書きます
そして test という部分で、取得した値を評価する関数を定義しています

構成情報とテストを同じファイルに記述できるため、コードから、よりサーバ要件を理解しやすくなります

Ansibleの利点

ここまでで、個人的にAnsibleの至らないと感じている部分を挙げてきましたが、Ansibleでないとできないことも沢山あるので、思いつく限り記載しておきます

  • Network機器のmodule群が充実している
  • Windows系のmodule群が充実している
  • Jinja2のTemplateが使える
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

音に合わせて波形が動くビジュアライザーをブラウザ上で作ってみる【p5.js】

この記事はDeNA 21 新卒 Advent Calendar 2020の19日目の記事です。

はじめに

YouTubeにあるBGM系の動画で、音楽に合わせてかっこよく波形が動くビジュアルエフェクトを見かけたことはありませんか?

その多くは動画編集ソフトで作成されているものと思います。
しかし僕は、もっと手軽に作ることはできないものかと考えました。

自分の好きな曲に合わせて動くビジュアライザーが欲しい!
今回はそんな思いで、マイクに入力された音に合わせてブラウザ上で動作するオーディオビジュアライザーを作ってみました。

今回作ったもの(GIF画像)
spectrum.gif

[準備] p5.jsの導入

今回はp5.jsというOSSのJavaScriptライブラリを使用して実装しました。
p5.jsは、図形の描画が得意なProcessingというプログラミング言語のJavaScript版ライブラリです。
こちらからCDNのURLをコピーして、scriptタグで読み込めば導入は完了です。
今回はサウンドの処理も扱うので、アドオンのp5.sound.jsも読み込みます。

index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js" integrity="sha512-WIklPM6qPCIp6d3fSSr90j+1unQHUOoWDS4sdTiR8gxUTnyZ8S2Mr8e10sKKJ/bhJgpAa/qG068RDkg6fIlNFA==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/addons/p5.sound.min.js" integrity="sha512-wM+t5MzLiNHl2fwT5rWSXr2JMeymTtixiw2lWyVk1JK/jDM4RBSFoH4J8LjucwlDdY6Mu84Kj0gPXp7rLGaDyA==" crossorigin="anonymous"></script>

詳しいチュートリアルは公式のGet Startedを参照してください。

シンプルなオーディオスペクトラムを作る

初めに、マイクに入力された音に反応するスペクトラムを実装してみます。

spectrum.png

p5.jsが読み込まれると、初めにsetup()が実行されます。
setup関数の中に、初期設定について書いていきます。

let mic, fft;

// 最初に実行される
function setup() {
  // 描画範囲を画面サイズいっぱいに設定する
  let cnv = createCanvas(windowWidth, windowHeight);

  // 画面がクリックされたらAudioの取り扱いを開始する。
  cnv.mouseClicked(userStartAudio);

  // マイクを利用するための初期設定
  mic = new p5.AudioIn();
  mic.start();

  // fftを利用するための初期設定
  fft = new p5.FFT(0.9, 256);
  fft.setInput(mic);

  // 図形の塗りつぶしを無効にする設定
  noFill();
  // 図形の線の色を255(白)に設定
  stroke(255);
}

最初にsetup()が実行されたあとは、draw()が繰り返し実行されます。
アニメーションのフレームが更新されていくようなイメージです。
draw関数の中に、描画の処理を書いていきます。

function draw() {
  // 背景を0(黒)で塗りつぶす
  background(0);
  // スペクトラムを描画する関数を呼び出す
  showSpectrum(fft);
}

function showSpectrum(fft) {
  const spectrum = fft.analyze();

  beginShape();
  for (let i = 0; i < spectrum.length; i++) {
    // map関数でiを0からspectrum.lengthの範囲から、0からwidthの範囲に置き換える
    // x = i / (spectrum.length - 0) * (width - 0) と同義
    // 詳細:https://p5js.org/reference/#/p5/map
    const x = map(i, 0, spectrum.length, 0, width);

    const amp = spectrum[i];
    const y = map(amp, 0, 255, height / 2, 0);

    // x, yの位置に頂点を打つ
    vertex(x, y);
  }
  // beginShape()からendShape()の間で打たれた頂点は線で結ばれる
  endShape();
}

実行例(CodePenリンク)
※ブラウザからマイクの利用許可を求めるメッセージが表示されたら「許可」を押してください。
黒い画面をクリックするとスペクトラムが動作開始します。

スペクトラムを円形にしてみる

circle_spectrum.png

showSpectrum関数を書き換えて、スペクトラムを円形に描画してみます。

// setup()は変更なし

function draw() {
  background(0);
  // 座標(x, y)=(0, 0)の位置を画面中央に設定
  translate(width / 2, height / 2);
  showSpectrum(fft);
}

function showSpectrum(fft) {
  const spectrum = fft.analyze();

  beginShape();
  for (let i = 0; i < spectrum.length; i++) {
    const angle = map(i, 0, spectrum.length, radians(0), radians(360));
    const amp = spectrum[i];
    const r = map(amp, 0, 255, 50, 200);
    const x = r * cos(angle);
    const y = r * sin(angle);
    vertex(x, y);
  }
  endShape();
}

実行例(CodePenリンク)
※ブラウザからマイクの利用許可を求めるメッセージが表示されたら「許可」を押してください。
黒い画面をクリックするとスペクトラムが動作開始します。

スペクトラムを反転させてくっつけてみる

mirror_circle_spectrum.png

直線状のスペクトラムを円形に丸めただけだと、開始地点と終了地点が繋がらず分断されてしまいました。
無理やり線でつなぐこともできますが、今回は180度分の円弧2つを左右反転させてくっつけてみることにします。

// setup()は変更なし

function draw() {
  background(0);
  translate(width / 2, height / 2);
  // スペクトルの開始角と終了角を引数として渡す
  showSpectrum(fft, -90, 90);
  showSpectrum(fft, 270, 90);
}

function showSpectrum(fft, startAngle, endAngle) {
  const spectrum = fft.analyze();

  beginShape();
  for (let i = 0; i < spectrum.length; i++) {
    const angle = map(i, 0, spectrum.length, radians(startAngle), radians(endAngle));
    const amp = spectrum[i];
    const r = map(amp, 0, 255, 50, 200);
    const x = r * cos(angle);
    const y = r * sin(angle);
    vertex(x, y);
  }
  endShape();
}

実行例(CodePenリンク)
※ブラウザからマイクの利用許可を求めるメッセージが表示されたら「許可」を押してください。
黒い画面をクリックするとスペクトラムが動作開始します。

ビートに合わせてスペクトラムをバウンスさせてみる

bounce_mirror_circle_spectrum.png

曲の盛り上がりに合わせて、スペクトルの円の半径を大きくしたり小さくしたりしてみます。
静止画像ですとイメージが伝わりづらいですが、このエフェクトによって臨場感が増すと思います。

ビートの検出にはp5.jsのpeakDetectを利用します。(p5.PeakDetect

let mic, fft, peakDetect;
let minRadius = 50;
let radius = minRadius;

function setup() {
  let cnv = createCanvas(windowWidth, windowHeight);
  cnv.mouseClicked(userStartAudio);

  mic = new p5.AudioIn();
  mic.start();

  fft = new p5.FFT(0.9, 256);
  fft.setInput(mic);

  peakDetect = new p5.PeakDetect(20, 2000, 0.5, 10);

  noFill();
  stroke(255);
}

function draw() {
  background(0);
  translate(width / 2, height / 2);

  peakDetect.update(fft);

  if ( peakDetect.isDetected ) {
    // ビートが検知されている場合、半径を90に設定
    radius = 90;
  } else {
    // ビートが検知されていない場合、半径を毎フレーム1ずつ小さくする
    radius -= 1;
    // minRadiusよりは小さくしない
    radius = (radius > minRadius) ?  radius : minRadius;
  }

  showSpectrum(fft, radius, radius+60, -90, 90);
  showSpectrum(fft, radius, radius+60, 270, 90);
}

function showSpectrum(fft, minR, maxR, startAngle, endAngle) {
  const spectrum = fft.analyze();

  beginShape();
  for (let i = 0; i < spectrum.length; i++) {
    const angle = map(i, 0, spectrum.length, radians(startAngle), radians(endAngle));
    const amp = spectrum[i];
    const r = map(amp, 0, 255, minR, maxR);
    const x = r * cos(angle);
    const y = r * sin(angle);
    vertex(x, y);
  }
  endShape();
}

今回の完成品

実行例(CodePenリンク)
※ブラウザからマイクの利用許可を求めるメッセージが表示されたら「許可」を押してください。
黒い画面をクリックするとスペクトラムが動作開始します。

終わりに

本記事ではp5.jsを使って、ブラウザでリアルタイムに動作するオーディオビジュアライザーを作ってみました。
今回はシンプルな実装にとどまりましたが、かっこいい背景画像を設定したり、カラフルなエフェクトを追加したりで自分好みのビジュアライザーを作成できそうです。
もし何かの参考にしていただけたら嬉しい限りです。

この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ LGTM、Twitter や Facebook、はてなブックマークにてコメントをお願いします!

また DeNA 公式 Twitter アカウント @DeNAxTech では、 Blog 記事だけでなく色々な勉強会での登壇資料も発信しています。ぜひフォローして下さい!
Follow @DeNAxTech

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

SVG + CSS + Node.js + receiptline で電子レシートを発行してみよう

マークダウン言語で紙のレシートや電子レシートを簡単に作れる receiptline。
https://github.com/receiptline/receiptline
https://www.npmjs.com/package/receiptline

今回は receiptline 本来の用途と考えられる、電子レシートの発行です!

レシートを設計する

デザインツール

ReceiptLine Designer を使います。使い方はこの連載記事の初回から。
いますぐ試したい方は、開発元のホームページで公開されているのでこちらへ。
01.png

ロゴの背景を透明に

以前の記事で作成したロゴ画像を再利用します。
02.png

GIMP の「色を透明度に」 (Color to Alpha) 機能で、背景色を白から透明にしました。
03.png

2023年度インボイス制度対応

前回の記事で学習した、簡易適格請求書等の記載事項も追加しておきます。

  • ① 適格請求書発行事業者の氏名又は名称及び登録番号
  • ② 課税資産の譲渡等を行った年月日
  • ③ 課税資産の譲渡等に係る資産又は役務の内容(課税資産の譲渡等が軽減対象資産の譲渡等である場合には、資産の内容及び軽減対象資産の譲渡等である旨)
  • ④ 課税資産の譲渡等の税抜価額又は税込価額を税率ごとに区分して合計した金額
  • ⑤ 税率ごとに区分した消費税額等又は適用税率

04.png

作成したレシートデータ

ReceiptLine
{image:iVBORw0KGgoAAAANSUhEUgAAASAAAAAwAgMAAADMTE88AAAACVBMVEVwAAsAAAD///9xeVj9AAAAAXRSTlMAQObYZgAAAdZJREFUSMftlsFu4zAMRMUD73sI/0c59M4Amv//lXJI2XEWaGSgAVrsxuihtqgnmjOk09r7+pcv7esYAEtMM19yrCkWxwnOgEbk1Fc5nwBpRNgCpGdAhKi/4NVYaFmD1qqlYi8ACU6Y6BRo/DaQjk3hiARv4BeB834aVXBDk1bL0lHlCEs0Qz5zP4A89FVW3rgWNwGxWrRuCVIC7Ao3HjBCamY6mt7GHSQ9/rnQVEGZIBmlBPL0Fsvoig8Mg9N6KJAAB5ASFPDYzUcJytgEFXAQzpf2/ajMCMeMjCAwMX8A9ZQjQUJQ7mZs5G4TZMcaXRJd3bKBLOZLz4gC9Q00Y8030FE1Vga5egf5put0rTwDbT4SalV6fQmyp6DZIqCONXe+Aqk/A82mlcF3RO7eQeMRZH0JElePJ3P336BNfjwtdg42CV9PHynu8ldjSO7NIhxAD/Lvo1YDZONPgdIlg26JvwT4BAmud1AZstsOkhz+Fo3pERHhdLxiMCy9z89VgcAmnaCyPfCxg2hqnn4NWr/MtWgD0Fdeqt9qLAwbOyiaNtXRXV3eDeZlfAmKlPrxK6WbD2umR02i0diyyBmTdVEmvhhR3//Mv0H/M8jwIhBWv7ve189enyEmqNlnG50wAAAAAElFTkSuQmCC}
柳都市星降町7丁目8番9号
登録番号 T1234567890123

2020年12月16日(水)12:34  #0903
{border:line; width:22}
^領 収 書
{border:space; width:3,*,3,8; text:nowrap}
166003 |2021葱鏡餅 水引 | 3個| ¥1,620*
691004 |洗面器45RPM N025 | 1個| ¥1,210~
-
{width:auto; text:wrap}
小 計 |4点 | ¥2,830~
(税率10%対象 | ¥1,210)
(内消費税等10% | ¥110)
(税率 8%対象 | ¥1,620)
(内消費税等 8% | ¥120)
-
合 計 | ^¥2,830
お預り | ^¥3,000
お釣り | ^¥170
|*印は軽減税率対象商品です
{code:202012160903; option:code128,2,48}

電子レシート発行サーバーを作る

Node.js

Node.js で電子レシートを発行する HTTP サーバーを作ります。
receipt.js の内部構成は、以下のようになっています。

  • ReceiptLine
    • 作成したレシートデータ (固定) を SVG に変換する
  • HTML
    • 作成した SVG を HTML に埋め込む
  • HTTP サーバー
    • 任意の GET リクエストを受けて、作成した HTML を返す
receipt.js
const http = require('http');
const receiptline = require('receiptline');

// ReceiptLine
const text = `{image:iVBORw0KGgoAAAANSUhEUgAAASAAAAAwAgMAAADMTE88AAAACVBMVEVwAAsAAAD///9xeVj9AAAAAXRSTlMAQObYZgAAAdZJREFUSMftlsFu4zAMRMUD73sI/0c59M4Amv//lXJI2XEWaGSgAVrsxuihtqgnmjOk09r7+pcv7esYAEtMM19yrCkWxwnOgEbk1Fc5nwBpRNgCpGdAhKi/4NVYaFmD1qqlYi8ACU6Y6BRo/DaQjk3hiARv4BeB834aVXBDk1bL0lHlCEs0Qz5zP4A89FVW3rgWNwGxWrRuCVIC7Ao3HjBCamY6mt7GHSQ9/rnQVEGZIBmlBPL0Fsvoig8Mg9N6KJAAB5ASFPDYzUcJytgEFXAQzpf2/ajMCMeMjCAwMX8A9ZQjQUJQ7mZs5G4TZMcaXRJd3bKBLOZLz4gC9Q00Y8030FE1Vga5egf5put0rTwDbT4SalV6fQmyp6DZIqCONXe+Aqk/A82mlcF3RO7eQeMRZH0JElePJ3P336BNfjwtdg42CV9PHynu8ldjSO7NIhxAD/Lvo1YDZONPgdIlg26JvwT4BAmud1AZstsOkhz+Fo3pERHhdLxiMCy9z89VgcAmnaCyPfCxg2hqnn4NWr/MtWgD0Fdeqt9qLAwbOyiaNtXRXV3eDeZlfAmKlPrxK6WbD2umR02i0diyyBmTdVEmvhhR3//Mv0H/M8jwIhBWv7ve189enyEmqNlnG50wAAAAAElFTkSuQmCC}
柳都市星降町7丁目8番9号
登録番号 T1234567890123

2020年12月16日(水)12:34  #0903
{border:line; width:22}
^領 収 書
{border:space; width:3,*,3,8; text:nowrap}
166003 |2021葱鏡餅 水引 | 3個| ¥1,620*
691004 |洗面器45RPM N025 | 1個| ¥1,210~
-
{width:auto; text:wrap}
小 計 |4点 | ¥2,830~
(税率10%対象 | ¥1,210)
(内消費税等10% | ¥110)
(税率 8%対象 | ¥1,620)
(内消費税等 8% | ¥120)
-
合 計 | ^¥2,830
お預り | ^¥3,000
お釣り | ^¥170
|*印は軽減税率対象商品です
{code:202012160903; option:code128,2,48}`;
const svg = receiptline.transform(text, { cpl: 32, encoding: 'cp932', spacing: true });

// HTML
const style = 'float: left; padding: 24px; background: lavender;';
const html = `<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>レシート</title>
    </head>
    <body>
        <div style="${style}">${svg}</div>
    </body>
</html>`;

// HTTP Server
const server = http.createServer((req, res) => {
    switch (req.method) {
        case 'GET':
            res.end(html);
            break;
        default:
            res.end();
            break;
    }
});
server.listen(8080, "127.0.0.1", () => {
    console.log('Server running at http://127.0.0.1:8080/');
});

実行

電子レシート発行サーバーを起動します。

$ node receipt.js

Web ブラウザーで localhost:8080 を開きます。
receiptline ライブラリが動作しない IE11 でも表示 OK。
05.png

作成された SVG データ

レシートデータから生成された SVG データです。
中身はパス、画像、テキスト、Web フォント、フィルター、いろいろ入っています。

SVG
<svg width="384px" height="684px" viewBox="0 0 384 684" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><style type="text/css"><![CDATA[@import url("https://fonts.googleapis.com/css2?family=Kosugi+Maru&display=swap");]]></style><defs><filter id="receiptlineinvert" x="0" y="0" width="100%" height="100%"><feFlood flood-color="#000"/><feComposite in="SourceGraphic" operator="xor"/></filter></defs><g font-family="'Kosugi Maru', 'MS Gothic', 'San Francisco', 'Osaka-Mono', 'Courier New', 'Courier', monospace" fill="#000" font-size="24" dominant-baseline="text-after-edge"><g transform="translate(48,0)"><image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASAAAAAwAgMAAADMTE88AAAACVBMVEVwAAsAAAD///9xeVj9AAAAAXRSTlMAQObYZgAAAdZJREFUSMftlsFu4zAMRMUD73sI/0c59M4Amv//lXJI2XEWaGSgAVrsxuihtqgnmjOk09r7+pcv7esYAEtMM19yrCkWxwnOgEbk1Fc5nwBpRNgCpGdAhKi/4NVYaFmD1qqlYi8ACU6Y6BRo/DaQjk3hiARv4BeB834aVXBDk1bL0lHlCEs0Qz5zP4A89FVW3rgWNwGxWrRuCVIC7Ao3HjBCamY6mt7GHSQ9/rnQVEGZIBmlBPL0Fsvoig8Mg9N6KJAAB5ASFPDYzUcJytgEFXAQzpf2/ajMCMeMjCAwMX8A9ZQjQUJQ7mZs5G4TZMcaXRJd3bKBLOZLz4gC9Q00Y8030FE1Vga5egf5put0rTwDbT4SalV6fQmyp6DZIqCONXe+Aqk/A82mlcF3RO7eQeMRZH0JElePJ3P336BNfjwtdg42CV9PHynu8ldjSO7NIhxAD/Lvo1YDZONPgdIlg26JvwT4BAmud1AZstsOkhz+Fo3pERHhdLxiMCy9z89VgcAmnaCyPfCxg2hqnn4NWr/MtWgD0Fdeqt9qLAwbOyiaNtXRXV3eDeZlfAmKlPrxK6WbD2umR02i0diyyBmTdVEmvhhR3//Mv0H/M8jwIhBWv7ve189enyEmqNlnG50wAAAAAElFTkSuQmCC" x="0" y="0" width="288" height="48"/></g><g transform="translate(0,72)"><text x="54,78,102,126,150,174,198,210,234,258,270,294,306">柳都市星降町7丁目8番9号</text></g><g transform="translate(0,102)"><text x="54,78,102,126,150,162,174,186,198,210,222,234,246,258,270,282,294,306,318">登録番号&#xa0;T1234567890123</text></g><g transform="translate(0,132)"><text x="0">&#xa0;</text></g><g transform="translate(0,162)"><text x="12,24,36,48,60,84,96,108,132,144,156,180,192,216,228,240,252,264,276,288,300,312,324,336,348,360">2020年12月16日(水)12:34&#xa0;&#xa0;#0903</text></g><g transform="translate(48,192)"><text x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276">╔══════════════════════╗</text></g><g transform="translate(48,216)"><text transform="scale(1,1)" x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276">&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;</text><text transform="scale(2,1)" x="24,48,60,84,96">&#xa0;&#xa0;</text></g><g transform="translate(48,240)"><text x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276">╚══════════════════════╝</text></g><g transform="translate(0,270)"><text x="0,12,24">166</text><text x="48,60,72,84,96,120,144,168,180,204">2021葱鏡餅&#xa0;水引</text><text x="240,252">3個</text><text x="300,312,324,336,348,360,372">¥1,620*</text></g><g transform="translate(0,300)"><text x="0,12,24">691</text><text x="48,72,96,120,132,144,156,168,180,192,204,216">洗面器45RPM&#xa0;N02</text><text x="240,252">1個</text><text x="300,312,324,336,348,360,372">¥1,210&#xa0;</text></g><g transform="translate(0,330)"><text x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276,288,300,312,324,336,348,360,372">════════════════════════════════</text></g><g transform="translate(0,360)"><text x="0,24,48">小 計</text><text x="132,144">4点</text><text x="300,312,324,336,348,360,372">¥2,830&#xa0;</text></g><g transform="translate(0,390)"><text x="0,12,36,60,72,84,96,120">(税率10%対象</text><text x="300,312,324,336,348,360,372">¥1,210)</text></g><g transform="translate(0,420)"><text x="0,12,36,60,84,108,132,144,156">(内消費税等10%</text><text x="324,336,348,360,372">¥110)</text></g><g transform="translate(0,450)"><text x="0,12,36,60,72,84,96,120">(税率&#xa0;8%対象</text><text x="300,312,324,336,348,360,372">¥1,620)</text></g><g transform="translate(0,480)"><text x="0,12,36,60,84,108,132,144,156">(内消費税等&#xa0;8%</text><text x="324,336,348,360,372">¥120)</text></g><g transform="translate(0,510)"><text x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276,288,300,312,324,336,348,360,372">════════════════════════════════</text></g><g transform="translate(0,540)"><text x="0,24,48">合 計</text><text transform="scale(2,1)" x="120,132,144,156,168,180">¥2,830</text></g><g transform="translate(0,570)"><text x="0,24,48">お預り</text><text transform="scale(2,1)" x="120,132,144,156,168,180">¥3,000</text></g><g transform="translate(0,600)"><text x="0,24,48">お釣り</text><text transform="scale(2,1)" x="144,156,168,180">¥170</text></g><g transform="translate(0,630)"><text x="0,12,36,60,84,108,132,156,180,204,228,252,276">*印は軽減税率対象商品です</text></g><g transform="translate(91,636)"><path d="M0,0h4v48h-4zM6,0h2v48h-2zM12,0h6v48h-6zM22,0h4v48h-4zM30,0h2v48h-2zM36,0h6v48h-6zM44,0h4v48h-4zM52,0h2v48h-2zM58,0h6v48h-6zM66,0h2v48h-2zM70,0h4v48h-4zM78,0h6v48h-6zM88,0h2v48h-2zM94,0h6v48h-6zM102,0h4v48h-4zM110,0h4v48h-4zM118,0h2v48h-2zM124,0h2v48h-2zM132,0h2v48h-2zM138,0h2v48h-2zM144,0h4v48h-4zM154,0h4v48h-4zM162,0h2v48h-2zM166,0h6v48h-6zM176,0h4v48h-4zM186,0h6v48h-6zM194,0h2v48h-2zM198,0h4v48h-4z" fill="#000"/></g></g></svg>

電子レシートの背景をカスタマイズする

ソースコードの CSS を変更して、電子レシートの背景をカスタマイズします。

単色

お店のキャンペーンでよく使われるピンクレシートと黄色いレシート。

receipt.js
const style = 'float: left; padding: 24px; background: pink;';

06.png

receipt.js
const style = 'float: left; padding: 24px; background: #ff9;';

07.png

多色

サーマルロール紙にはできないグラデーション。

receipt.js
const style = 'float: left; padding: 24px; background: linear-gradient(skyblue, lightyellow);';

08.png

receipt.js
const style = 'float: left; padding: 24px; background: linear-gradient(lightgreen, snow, beige);';

09.png

透かし

「複写」「COPY」の画像をタイル状に並べます。画像は CSS に埋め込みます。
10.png

画像の Data URI 形式への変換は ReceiptLine Designer を使うと簡単です。
画像をロードして image:data:image/png;base64, に置き換えます。
11.png

receipt.js
const style = 'float: left; padding: 24px; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANgAAADYAQMAAACz7r+uAAAABlBMVEXc3tv5+/j5MJA8AAADWklEQVRYw+2YPW7bMBSAqTAwMxTmBVwqR2g3G1AiH8W9gYMsCuLGSg3UW3yBwDlElwLNYMGDlza+QFsoMFqvDAy0NCpENQqRj3yCshfRmyh9En8eH98PCamllucih0+wPjT3MFPQZAh5GbQ5YvQ7tH3E2ATaAWK8Ae0MMXEAXUjEgpZpDjGLzCA0TxGTkelSIeal0tP/XSJGk9Uo1t/N0PJGi0UV478nS9MHYiL3Hw2L0fIuWpnF9t3lRQrGTps2GwQS2Fg5O3H88h765NmxzXx//tXowRcDZw18PIVJc2WrpcG5ME8tZ5cYYQKYoLZGOWkEwJpsZg2429hoYA1+mQPrxATWR9gNy0FvpzMvBcOmj3xu2It1aivfy3zYJ38pma18dUzMTP2pcuy8HwDjt1nDOW+Rxc6u8YEzjG3ayCI9YD/P4wrWYNnaZbAk7m8XLuuxpHjB8uHNO3cufFJ0SrMwv3KZr7d3p6M/U2SSLNPsy8Y9q35GivNIPydr5R4BRQoTpVcJOo+7JWjGt3mMfZE2bb5dILJnGNu+x+4mNiy5wgz+W01LTBhvc11i2qewqF1i2lxZEJXmopVBRR+7zEtZ5U2JB4piJbXksyq3sXOG0EVcB8Faaqmllv9TIGW5KMWHGUSKEksh/S3FAAg5CUpHyX7U1SzGaXMz0NFLEMyEiWwBwTGuxWUl6yw0i0p9vp7LcupUyInFephNZLnG0WFzIrsVjOajdjFMm3C3nuCrUccw381twsWHT4UaRT+8ddjbpfILxuXAydLJ2bdMMyaVW2e92T7qisZLpMu8zU2mZ0dTxNZh2jN5Fard7k7BKGLEWmsJuQbKRI7u+sCEW2CG1+Y/SkTDzSM7CsYLnE4l31gs82Pbctn6o3lIVAgm2qAzurT2Mo3ALDiLPSuZG85k36pXnJoztGuEQPRI5GSjkNq1A0mcaia1WCQPbcPj0mIPst+1D0cEIxytlJMfBqGCMmRx65TGEZdQhkxajur7LAUmhEBlP7AgDKpcQ6iGUZVLOZe/ZBV7uP+BS2PD0vkc+RQa6xddNka+iI6hXirdJkD66y7vX0UDllu6TYB5H+BU3DK0V/jGwKqFT7C7GdiWVX1ZEsZVnrZ8IWK7reYTAWC/joG1PBv5C4viMg7LEHCAAAAAAElFTkSuQmCC);';

12.png

地紋

領収書やチケットでよく見かける地紋です。小さな素片をタイル状に並べます。
13.png

const style = 'float: left; padding: 24px; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADAQMAAACplL1tAAAABlBMVEX/pQD////52iT3AAAAFElEQVQI12M4l/+AYf/mCQyFdwoAJNIF3T0xRTsAAAAASUVORK5CYII=);';

14.png

また何か作ったら投稿します。ではまた!

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

ReactでAirtableのAPPS(アプリ)を作ろう!!

みなさんこんにちは。
NTTテクノクロスの上原です。業務ではGatsbyを使って内製キュレーションサイトの構築や運用などを行なっています(関連で一昨年書いた記事→Gatsbyの真の力をお見せします)。
これは、NTTテクノクロスアドベンドカレンダー2020 の20日目の記事です。昨日の記事は @y-ohnuki さんによる「iPhone12ProのLiDARスキャナを試してみる」でした。

去年はこんな記事(React-SpringのHooks APIでブラウザアニメーションを基本から極めよう!)を書いてたわけですが、今年は新型コロナでいろいろとたいへんだった年でした。みなさまは、いかがお過しだったでしょうか。一年があっという間ですね。来年は何をしているのだろうか。

今年はノーコード・ローコード開発サービスであるAirtableのAPPS機能をプログラマ目線で紹介します。

対象読者

  • ノーコード開発ツールに興味がある技術者
  • マイクロフロントエンド的にReactアプリを結合させて機能させることに興味がある人
  • 未来のアプリ開発のやりかたについてインスピレーションを得たい人

Airtableって何?

Airtableはブラウザから利用できるスプレッドシートのインターフェースを持ったオンラインデーターベースサービスです。カレンダーやカンバン的に表示したり、スケジュール管理に使ったり、ワークフローを組み合わせたりもでき、スプレッドシートの自然な拡張として、専用の業務アプリが提供するような機能を、非プログラマでも直感的かつ柔軟に達成できるサービスであり、アプリ開発環境でもあります。

Airtableは2013年からの老舗でありますが、最近流行りの「ノーコード開発プラットフォーム」の代表とも言え、2020年9月にも$185Mという大規模な資金調達を成功させ、評価額25億8,500万米ドルを誇るユニコーン企業としても注目を集めています。

Airableはもともとはコードを一切書かない・書けない「ノー」コード開発ツールでしたが、2020年9月にリリースされたIFTTTライクなAutomation、JavaScript/TypeScriptによる機能拡張を可能にするAPPS(およびScripting APPS)の提供によってローコード開発ツールに展開されたことになります。

ビジネスとして、またツールの有用性としてAirtableの成功は興味深いものですが、「未来のWebアプリ開発」を見通すことに関してソフトウェアデザイン、エコシステムとしても非常に興味深いものです。本記事ではプログラマ目線でノーコード開発ツールの一つとして紹介し、印象をお伝えしたいと思います。

Airableサービスはどんな感じのものか?

Airableサービスにサインアップしてログインすると
1.png

こんな感じの画面。並んでいるアイコンは「ベース」と呼ばれる単位で、Excelでいうxlsファイル(ブック)に相当します。ベースをクリックすると以下のような、タブで切り替えられる「テーブル」の集合を見ることができます。

ベース:
2.png
ブラウザ上で、まずはOffice 365のWeb版ExcelやGoogle Spread Sheetのように直感的に使うことができます。ベースは概念的に以下のような構造をもっています。
3.png
ベースはテーブルの集合で、Excelでいえばシートをあつめたものがブックになる、というイメージです。ただしAirtableでは常に「ビュー」というレイヤを1層挟んでテーブルをユーザが見たり操作したりすることになります。

今までに出てきた用語を表にまとめると以下のとおりです。

Airtableの用語 Excelでの対応物 意味
ワークスペース ブックが格納されているフォルダ 共同作業者(コラボレータ)と共有できる単位のベースの集合。プロジェクト。
ベース ブック(.xlsx) テーブルの集合。1画面。テープルをタプで切り替えられる。
テーブル ワークシート レコードの集合
レコード ワークシートの1行 テーブルの1行
ビュー 対応物なし テーブルの見せかた。不要なものを隠蔽したりソートしたり
フィルタしたり。kanbanやカレンダーの
ビューなどに対応。
APPS 対応物なし(あえて言え
ばグラフ領域やピボット
テーブル領域)
ミニアプリ。

Airtableの特徴はなんなの?

以下が特徴になります。

  1. データモデリングを出発点かつ中心としたツール。
    • レコードの集合としてのテーブルを定義していく。レコードのフィールドは静的な型を持っている。
    • SQLが不要なリレーショナルデータベースでもある。 リレーションやカーディナリティ(1:n、m:n…)を設定できる。
    • テーブル間の関係をExcelのセル操作のようなレベルでとても直感的に設定できる。
  2. Excelライクなわかりやすいユーザインターフェースで扱う
    • 機能の大半はExcelのような汎用インターフェースで達成する。一般アプリとまったく同じ使い勝手のUIはめざさない。
  3. その他
    • 実データを活用したRest APIと参照ドキュメントがリアルタイムに生成される。実際のデータを元にしてサンプル出力などを表示するのでわかりやすい。
    • APPS(後述)で拡張可能。一例としてGraphQL APIも生成できる(BaseQL APP)。

「ノーコード開発ツール」といってもいろいろですが、Airtableの方向性は明確です。それは、現実社会で一般に、業務の多くがExcelのような表計算でまかなえていることに着目し、その延長・拡張として機能実現することです。考えてみましょう、一般の事務職や会社員が、いかにExcelとファイル共有という汎用インターフェースだけで多くの業務をこなしてきているかを。このコンセプトによってAirtableは圧倒的なとっつきの良さ、開発速度と機能カバーを実現しています。

なお、本記事で言及しているのはAirtableのごく一部です。Gatsbyのデータソースに使えるなど、連携機能も豊富です。

ノーコード開発ツールについて少しだけ

字面から言えば、「ソースコード」を一切書き下さずにソフトウェアを開発するものがノーコード開発ツールであり、コードを書く量が少ないものの、ゼロではないものがローコード開発ツールです。とはいえ境界はあいまいです。

現代的な意味のノーコード・ローコード開発ツールは、一般にクラウドサービスとして提供されていることが必要です。いわゆる「ビジュアルプログラミング言語」であればノーコード開発ツールと言えるかというと多分違うのであって、データ管理や実行環境を含めてクラウドサービスとして機能を利用できることが従来からのツールとの違いであり、わざわざ「ノーコード・ローコード」と新しい名前で呼ぶ理由の一つであると言えます(例外はたぶんありますが)。

つまり、ソフトウェアライフサイクルのうちのコーディングだけではなく、ビルド、デプロイ、テスト支援といった開発支援系機能を統合サービスとしてクラウド上で提供するものです。

とはいえ、従来からあったもののリブランデイングで呼び名が新しいだけ、という面もあるのもたぶん確かです。

Airtable APPS(アプリ)

APPSはReactで書く「ミニアプリ」です。

AirtableがExcelだとしたら、Excel中に埋め込める「グラフ領域」や、「ピボットテーブル領域」を想像してみると少し近いです。実際、Airtableのピボットテーブルやグラフ機能はAPPSとして利用できます。なお、APPSは有償のPro Planのみで使用できる機能です(ただ2020年12月現在、登録後2週間はPro Plan無料で利用できるようです)。ただし、自分でビルドして利用するカスタムAPPSは、今のところ無料版(Free Plan)でも開発したり使用することができるようです(ただし保証や将来も利用できるかものかなどは不明)。なお、APPSは以前はBlocksという名前でしたが、名前が変更されました。以降で時々出てくるblockはAPPSと同義です。

APPSの実行の様子は以下のとおりです。

4.png

APPSは「ダッシュボード」の中にまとめて表示でき、利用者はダッシュボードをカスタマイズしてそれを表示することでノーコードプラットフォームとしてのAirableをAPPSを通じて利用することができます。

Appsは個別にAirtableのベースに追加し、Airtableの中で実行して使用します。単独では使用できません。

ReactでAPPSを作ろう!! ???

以上は前置きでした。以降が本題です。早速カスタムAPPSを作ってみましょう。APPSはFirebaseのようにリアルタイムデータ更新を扱えますので、チャットアプリを作ってみます。

完成予想図はこんな感じです:
スクリーンショット 0002-12-16 17.36.00.png

データモデリング

Airtableの開発の流儀としてデータモデリングを最初にやります。ここでは「発言内容」「作成者」などのフィールドを持ったテーブルを定義し、それにチャットアプリをアタッチできるようにします。

準備として、まずベースを新規作成(ベース名「チャット」、アイコンを適当に設定)を作成し、デフォルトで作成されているテーブルを編集し、テーブル名「発言一覧」にして、レコードヘッダの「Customize Field Type」を選択して以下のようなテーブルを作成します。

フィールド名 Type 内容 GUI上での入力内容
ID(Primary Field) Autonumber - f1.png
日時 Formula CREATED_TIME() f2.png
内容 Long text - f3.png
発言者 Created By - f4.png

以上より、以下のような「発言一覧」テーブルが作成されます。

5.png

ここでは使用しませんでしたが、フィールドに「Link to another record」という型を指定することで、他のテーブルのフィールドの値をRDBで言うところの外部キーとして使用することなどができます。

ひながた生成

では自作のAPPSである「カスタムAPPS」を作成していきましょう。以下の操作を行います。

  1. 準備として、画面右上の「Account」メニューから「Account」を選び、表示されるAPIキー(★1)を確認します。
  2. ベースの画面、右側の「APPS」をクリック
  3. 「Install an app」をクリック
  4. 「Build a custom app」をクリック
  5. テンプレートギャラリーから「Hello world (TypeScript)」をチェック
  6. 「App name」に「Chat」を入力、「Creating App」ボタンをクリック

以降、表示されるガイダンスに従います。

  1. ターミナルで「npm install -g @airtable/blocks-cli」を実行
  2. 同じくターミナルでAPPSの雛形の生成を行います。(なお現在のblock initはProxy背後ではエラーになるかも)
block init appO9XXXXXXXXXXXX/blkYYYYYYYYYYYYYY --template=https://github.com/Airtable/apps-hello-world chat
  1. 初回は以下が表示されるので、(★1)で用意していたAPIキーを入力します。
? Please enter your API key. You can generate one at https://airtable.com/account

これでうまくいけばAPPSアプリのソースコード雛形が作成されます。

Using your existing API key from /Users/uehaj/.config/.airtableblocksrc.json
Initializing block using https://github.com/Airtable/apps-hello-world-typescript template
[npm]
[npm] > core-js@3.8.1 postinstall /Users/uehaj/work/lowcodenocode/airtable/air_chat/node_modules/core-js
[npm] > node -e "try{require('./postinstall')}catch(e){}"
[npm]
[npm] added 225 packages from 225 contributors and audited 225 packages in 55.392s
[npm]
[npm] 23 packages are looking for funding
[npm]   run `npm fund` for details
[npm]
[npm] found 0 vulnerabilities
[npm]
✅ Your block is ready! cd air_chat && block run to start developing, and npm run lint to lint.

カスタムAPPSの実行

一旦以下を行いAPPSを実行してみます。

cd chat
block run

うまく行けば以下が表示されます。

6.png

  • ブラウザに戻って、「Continue」をクリックし、
    7.png

  • block runで表示されたURL、さっきの場合だと「https://localhost:9000 」を入力します(ポート番号は異なることがあり)

  • ブラウザでは以下のようにアプリが実行されています。

8.png

ソースを修正して保存すると自動リロードが走ります。

ReactコードとしてのAPPS

APPS SDK(Blocks SDK)が提供するのはReactをベースとしたSDKです。ただし開発できるアプリには以下の制限や特徴があります。

  • Airtable画面の一部の矩形領域パーツ(iframe)として実行される。メインメニューや自身の複雑なレイアウトはもたない。
  • アプリのデプロイとホスティングは気にしなくてよい。ローカルビルドのときはlocalhostでdev serverが動作するが、いったんblock releaseすればAirtableのサーバ内でホスティングされる。
  • webpackの設定など、こまかいことはできない。
  • 任意のnpmモジュールの使用が可能。
  • スタイルシステムはAirtable提供のもの(loadCSSromStringなど)を使用する。CSS in JSなどもやればできると思うが、全体のスタイルと不一致となるし、画面を占有できないし、一工夫が必要だと思われる。(あまり凝るべきではないのかもしれません)

APPSにはUIデザインガイドラインがあり、Airtableの中でよりよく機能を発揮するように、コンポーザブル(合成可能)で、柔軟性があり、協調的に動作する、といった指針が定義されています。

hello world APPSのコード

APPSの開発を始める前に、block buildコマンドでデフォルトとして生成されたHello Worldアプリ(flontend/index.tsx)の中身を見てみましょう。

import {initializeBlock} from '@airtable/blocks/ui';
import React from 'react';

function HelloWorldTypescriptApp() {
    // YOUR CODE GOES HERE
    return <div>Hello world ?</div>;
}

initializeBlock(() => <HelloWorldTypescriptApp />);

Create React Appで生成したReactアプリのようなReactDOM.render()を行なわないことに気付きます。その代わりにinitializeBlock()」にコンポーネントを渡します。それ以外は基本的には普通のRactアプリです。

Chatアプリ開発

ではチャットアプリを作っていきます。フォルダ・ファイル構成はこんな感じです。

chat/frontend
├── components
│   ├── ChatPanel.tsx
│   └── Setup.tsx
├── index.tsx
└── useConfig.tsx

./index.tsx

トップレベルに置くindex.tsxを置き換えて以下の内容のとおりにします。

index.tsx
import {
    Box,
    initializeBlock,
} from '@airtable/blocks/ui';
import React from 'react';
import ChatPanel from './components/ChatPanel';
import Setup from './components/Setup';

function ChatApp() {

    return (
        <Box flexDirection='row' display="flex">
        <Box flex="8" padding={3} ><ChatPanel /></Box>
            <Box flex="auto" ><Setup /></Box>
        </Box>
    );
}

initializeBlock(() => <ChatApp />);

index.tsx解説

ここで使用しているBoxはdivに展開されるコンポーネントです。flexboxの制御のためのパラメータを指定できます。ここらへんのUI部品群はMaterial UIなど既存のものではなく、Airtable独自のもののようです。

./useConfig.ts

アプリの設定情報にアクセスするためのhook。

import { useGlobalConfig } from '@airtable/blocks/ui';

const configKeys = [
  'selectedTableId',
  'selectedViewId',
  'selectedMessageFieldId',
] as const;

type ConfigKeys = typeof configKeys[number];

export default function useConfig() {
  const globalConfig = useGlobalConfig() as {
    get(key: ConfigKeys): string;
  };
  const selectedTableId = globalConfig.get('selectedTableId');
  const selectedViewId = globalConfig.get('selectedViewId');
  const selectedMessageFieldId = globalConfig.get('selectedMessageFieldId');

  return {
    selectedTableId,
    selectedViewId,
    selectedMessageFieldId,
  };
}

useConfig.ts解説

globalConfigはBlock SDKの特徴的な機能の一つで、APPSのインスタンスごとにサーバサイドに確保される設定情報を保存するストレージだと思ってください。キー名のハッシュとして任意の値を保存・取得できるのですが、型つきで扱うために、キーをas constした文字列配列にしています。

ここで保存しているのは、アプリと、テーブルのビュー・カラムに対する紐付け情報です。具体的には、APPSの初回実行時に下図のように「どのテーブル」「どのビュー」「どのフィールド」かなどを処理対象として指定して紐付けます。その対応は上記の処理によって明示的にglobalConfigに保存します。

9.png

ちなみにGlobalConfigが壊れた場合にアプリが起動しなくなるなどがありえます。あるいはフィールドの紐付けをやりなおしたい場合は、その機能を作り込まなくてもAirableのUI(APPSの「Glocal config」)からClearを行うことができます。

./components/Setup.tsx

前述のGlobalConfigを設定するためのUIです。

import React, { useState } from 'react';

import {
  TablePickerSynced,
  ViewPickerSynced,
  FieldPickerSynced,
  FormField,
  Box,
  useBase,
} from '@airtable/blocks/ui';
import useConfig from '../useConfig';

export default function Setup() {
  const { selectedTableId } = useConfig();
  const base = useBase();
  const table = base.getTableByIdIfExists(selectedTableId);

  return (
    <>
      <Box padding={3} borderBottom="thick">
        <FormField label="テーブル">
          <TablePickerSynced globalConfigKey="selectedTableId" />
        </FormField>
        <FormField label="ビュー">
          <ViewPickerSynced table={table} globalConfigKey="selectedViewId" />
        </FormField>
        <FormField label="Created byフィールド">
          <FieldPickerSynced
            table={table}
            globalConfigKey="selectedCreatedByFieldId"
            placeholder="Pick a 'created by' field..."
          />
        </FormField>
        <FormField label="Messageフィールド" marginBottom={0}>
          <FieldPickerSynced
            table={table}
            globalConfigKey="selectedMessageFieldId"
            placeholder="Pick a 'message' field..."
          />
        </FormField>
      </Box>
    </>
  );
}

Setup.tsx表示

以下のように表示されます。

10.png

Setup.tsx解説

Airtable APIには以下のような、存在するテーブル/ビュー/フィールドをそれぞれ選択するための専用のGUI部品があり、それにGlobalConfigのキー名文字列を指定するだけでGloalConfig領域への読み書き含めて行なってくれます。

  • TablePickerSynced
  • ViewPickerSynced
  • FieldPickerSynced

いずれも、存在するテーブルやビュー名から選択するSelectユーザインターフェースで設定できます。

11.png

./components/ChatPanel.tsx

チャットのメイン画面です。

import React, { useState } from 'react';
import {
  useBase,
  useRecords,
  Box,
  Input,
  loadCSSFromString,
} from '@airtable/blocks/ui';
import useConfig from '../useConfig';

loadCSSFromString(`
.base {
  background-color: #34569b;
  padding: 0.5rem;
  border-radius: 10px;
}
.balloon {
  position: relative;
  display: block;
  margin: 0.5rem 100px 1.0rem 10rem;
  padding: 10px 10px 20px 10px;
  min-width: 120px;
  max-width: 100%;
  margin-left: 20px;
  color: #555;
  font-size: 16px;
  background: #e0edff;
  border-radius: 15px;
}

.balloon:before {
  content: "";
  position: absolute;
  top: 50%;
  left: -15px;
  margin-left: -10px;
  margin-top: -15px;
  border: 15px solid transparent;
  border-right: 15px solid #e0edff;
  z-index: 0;
}

.balloon p {
  margin: 0;
  padding: 0;
}
`);

export default function ChatPanel() {
  const {
    selectedTableId,
    selectedViewId,
    selectedCreatedByFieldId,
    selectedMessageFieldId,
  } = useConfig();
  const base = useBase();
  const table = base.getTableByIdIfExists(selectedTableId);
  const view = table ? table.getViewByIdIfExists(selectedViewId) : null;
  const messageField = view
    ? table.getFieldByIdIfExists(selectedMessageFieldId)
    : null;

  const records = useRecords(view, {
    fields: [selectedCreatedByFieldId, selectedMessageFieldId],
  });

  return (
    <Box className="base">
      {messageField && (
        <Box>
          <input
            onKeyPress={(e: any) => {
              if (e.key === 'Enter') {
                table.createRecordsAsync([
                  { fields: { [selectedMessageFieldId]: e.target.value } },
                ]);
                e.target.value = '';
                e.preventDefault();
                return false;
              }
            }}
            placeholder="発言をどうぞ"
            size={50}></input>
          {records &&
            records
              .slice()
              .sort((a, b) => b.createdTime.getTime() - a.createdTime.getTime())
              .map((msg) => (
                <div className="balloon">
                  <p>{msg.createdTime.toLocaleString()}</p>
                  <p>
                    {(msg.getCellValue(selectedCreatedByFieldId) as any).name}
                  </p>
                  <p>{msg.getCellValue(selectedMessageFieldId)}</p>
                </div>
              ))}
        </Box>
      )}
    </Box>
  );
}

ChatPanel.tsx表示

以下のように表示されます。

12.png

ChatPanel.tsx解説

以下、個別に説明します。

loadCSSFromString(`
.base {
  :
});

loadCSSFromString()でクラス名指定のスタイルシートを設定します。CSS in JS的な方法もおそらくは適用できるのでしょうが調べきれず。

  const {
    selectedTableId,
    selectedViewId,
    selectedCreatedByFieldId,
    selectedMessageFieldId,
  } = useConfig();

GlobalConfigからの読み込みを処理を行うカスタムHook、useConfig(前述)を使用して設定項目を取り出します。

  const base = useBase();

useBaseはAirableのベースを取得します。useStateの様に動作し、すなわち変更があったときだけ新しい値でレンダリングが行なわれます。

  const table = base.getTableByIdIfExists(selectedTableId);
  const view = table ? table.getViewByIdIfExists(selectedViewId) : null;
  const messageField = view
    ? table.getFieldByIdIfExists(selectedMessageFieldId)
    : null;

baseから順に、テーブル、ビュー、フィールド名を取得します。

  const records = useRecords(view, {
    fields: [selectedCreatedByFieldId, selectedMessageFieldId],
  });

最終的に表示したい発言のリストを、使用したいフィールドを指定して取得します。

          <input
            onKeyPress={(e: any) => {
              if (e.key === 'Enter') {
                table.createRecordsAsync([
                  { fields: { [selectedMessageFieldId]: e.target.value } },
                ]);
                e.target.value = '';
                e.preventDefault();
                return false;
              }
            }}
            placeholder="発言をどうぞ"
            size={50}></input>

JSXではonKeyPressハンドラを設定した入力フィールドを用意します。Inputではなくinputを使っているのはonKeyPressハンドラが指定できなかったためですが原因不明。

          {records &&
            records
              .slice()
              .sort((a, b) => b.createdTime.getTime() - a.createdTime.getTime())
              .map((msg) => (
                <div className="balloon">
                  <p>{msg.createdTime.toLocaleString()}</p>
                  <p>
                    {(msg.getCellValue(selectedCreatedByFieldId) as any).name}
                  </p>
                  <p>{msg.getCellValue(selectedMessageFieldId)}</p>
                </div>
              ))}

発言を作成日時でソートして表示します。ビュー側でソートすることもできるのでき、ビューモデルとしてはその方が正しいかもしれませんが、ここではクライアント側でソートしておきます。

Chatアプリの実行???

上記のソースコードを保存すると、刻々とAirtableのAPPS領域のダッシュボード内に表示されるアプリが更新されていくと思います。完成したならば、Setupで表示されるフィールドに実際のベースのテーブル、ビュー、発言フィールドを選択させます。先に作成していたテーブルであれば、テーブルに「発言一覧」、ビューに「Grid View」、「Created Byフィールド」に「内容」、Messagesフィールドに「内容」を設定します。

13.png

するとアプリが結びついてチャットができるようになります。

airtable.mov.gif

Chatアプリのリリース?????

動作確認が済んだら以下を実行してアプリをAirbleのサーバサイドにリリースすることができます。

block release

リリースするとblock runをローカル実行する必要がなくなります。一回リリースすると、修正には再リリースが必要になります(localhostに立てたdev serverを使ってのリアルタイム更新はできなくなる)。

なお、カスタムAPPSをマーケットプレースに公開することもできるし(レビューあり)、Gibhubにソースコードを公開しておくことで他者にカスタムAPPSとしてビルドしてもらう前提で公開することもできます。本稿で作成したアプリは以下に公開しております。カスタムAPPSのBuild An Appのときに「Remix from GitHub」を選びURLを入力することで選択できる雛形として利用することができます。(やってるいることはgithubの指定プロジェクトの最新のソース内容を展開し、ベースとの紐付けのIDを含んだ.block/remote.json作成することです)

https://github.com/uehaj/airtable-chat

Airtableでのプログラミング、設計、開発について

Airtableを調査するにあたってプログラマとして気づいたことを列挙してみます。

(1)データモデリングから始めよう

一般に、Airtableでのアプリ開発ではデータモデリングから開始します。Airtableのデータモデリングとは、テーブルのフィールドの「型」を設定していくことです。これは常に実データを見ながらDBテーブルのスキーマ定義を行うことです。場合によってはAPPSを使用せずに業務が完結するかもしれません。APPS開発をするとしたら、アジャイル開発で求められる条件「最初の段階からミニマムな機能が動いていること」が達成できています。

(2) フォーミュラは副作用なしメソッド

Airtabbleのフィールドに設定できる型は多彩ですが、特徴的な型の一つに「フォーミュラ(Formula)」があります。これはExcelのセル式に対応するように見えますが、カラム全体の設定であることが大きな違いです。その違いが何を生んでいるかというと、レコードを「クラス定義」とみなしたとき、フォーミュラがメソッド定義の機能を担えることです。副作用がないのでデバッグは容易です。CQRS(コマンドクエリ責務分離)のクエリとも言えます。

APPSを組み合せるときでも、ビューまわりのための加工はフォーミュラとしてサーバサイドで実行してしまうことができます。SPAと組み合わせる場合、ビューモデルをサーバサイドで作っているということでもあります。

(3) REPLのように

実データをもとに、「動くデータ」を元にして、処理結果を見ながらアプリを開発していきます。

おわりに

クラウドの隆盛と開発技術の進展で、ツールやMbaaSなどを活用して、新規コード開発以外の方法で効率良く機能達成を行なうことが求められている時代を迎えています。プログラミングが授業で教えられる時代であり、アプリ利用とアプリ開発の垣根が下っていくことにも間違いありません。しかし、その先にあるものはプログラマとして見ても、あるいはプログラマ視点で見るからこそ、依然として豊かで興味深いものです。私はAirtaleのAPPSの例を通じて近未来のアプリ開発のありかたの一端を感じました。その一端が伝わればと思います。
ちなみにノーコード・ローコード開発の分野は奥が広く、Outsystems、Mendix、ほかさまざまな別の方向でそれぞれに別世界が開かれています。

明日は@nakasho-devさんの記事です。

では良いお年を!

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

【教材】JavaScriptの教材を作ってみました�2

※当方駆け出しエンジニアのため、間違っていることも多々あると思いますので、ご了承ください。また、間違いに気付いた方はご一報いただけると幸いです。

半人前のくせに、偉そうに教材を作ってみました。

学べる技術

  • 繰り返し構文
  • 多次元配列

問題1

console.table(array);

 //[ [1], [2, 3], [4, 5, 6], [7, 8, 9, 10] ]

この様な結果が返る配列arrayを繰り返し構文を用いて作ってください。(見やすいように半角スペースを入れてますが不要です)

問題2

let x = ["a", "b", "c"];
let y = ["d", "e", "f"];
let z = ["g", "h", "i"];

上記の様な配列があるとします。

下記の様な結果が返る配列X,Y,Zを繰り返し構文を用いて作ってください。

console.log(X);
console.log(Y);
console.log(Z);

//["a", "d", "g"];
//["b", "e", "h"];
//["c", "f", "i"];

問題1 回答例

let array = [];
let inArray = [];

let num = 1;
for (let i = 1; i < 5; i++) {
  for (let g = 0; g < i; g++) {
    inArray.push(num);
    num++;
  }
  array.push(inArray);
  inArray = [];
}
console.table(array);

問題2 回答例

let x = ["a", "b", "c"];
let y = ["d", "e", "f"];
let z = ["g", "h", "i"];

let X = [], Y = [], Z = [];

const array = [x, y, z];
const array2 = [X, Y, Z];

for (let i = 0; i < 3; i++) {
  for (let g = 0; g < 3; g++) {
    array2[i].push(array[g][i]);
  }
}
console.log(X);
console.log(Y);
console.log(Z);

※あくまで駆け出し半人前エンジニアが作った回答となります。あまり鵜呑みにしないで下さい。

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

javascriptにおけるテキストエリアへの追加と削除

1.はじめに

まずはhtmlコードの型

<input type="text" id="text1">
<input type="button" value="追加" onclick="addText();">
<input type="button" value="削除" onclick="deleteText();">
<br>
<textarea id="area1"></textarea>

次にjavascriptのコードの型

<script>   
    function addText() {

    }

    function deleteText() {

    }

2.プロセス

①まずは追加ボタンの入力値を代入。
 ※.valueでタイプした値を取得できる。

var ta1 = document.getElementById("text1").value;

②入力した値をテキストエリアに代入していく。
 ※"+="で次々に代入していける。

document.getElementById("area1").value += ta1;

③テキストを変数に代入して、変数の値を空白にする。

function deleteText() {
  let ta2 = document.getElementById("area1");
  ta2.value = "";
}

3. 完成形

function addText() {
  var ta1 = document.getElementById("text1").value;
  document.getElementById("area1").value += ta1;
}

function deleteText() {
  let ta2 = document.getElementById("area1");
  ta2.value = "";
}

以上

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

Flash Advent Calendar 12日目 - WebGL導入への道のり05 -

WebGLで色々な描画ができるようにはなってきたのですが
複雑な描画の実装を進めていくとGPUの負荷が高くなり、速度が低下してきました。

その時に起きた低下原因と解消方を書いていこうと思います。

  1. Framebufferを使っていない
  2. コール数が多い(createしたbuffer情報を使い回せていない etc...)
  3. 必要最低限の領域に対して描画を行う

Framebufferを使っていない

そもそもとして、メインのFramebufferだけで描画処理を行っていた...
オフスクリーンレンダリングができていない状態でした。

この時、WebGL1.0で実装したいたので、Framebufferを導入するとアンチエリアスが使えない!?
っという事態になりました。
WebGL2.0への移行が必須になりシェーダーをWebGL2.0に対応する課題が発生したのですが
それはまた別の機会に・・・orz

メインのFramebufferに描画するとcanvas側に描画が適用される為、無駄なI/Oが発生してしまいます。
サブのFramebufferは完全にメモリだけの描画になるため、負荷が軽減されます。

// 描画だけで使うFramebufferを生成
this.frameBuffer = this.gl.createFramebuffer();
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.frameBuffer);

// 書き込むtextureを生成
this.texture = this.gl.createTexture();
this.gl.activeTexture(this.gl.TEXTURE0);
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);

this.gl.framebufferTexture2D(
    this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0,
    this.gl.TEXTURE_2D, this.texture, 0
);

// 描画情報を書き込むRenderbufferを生成
this.stencilBuffer = this.gl.createRenderbuffer();

this.gl.renderbufferStorage(
    this.gl.RENDERBUFFER,
    this.gl.STENCIL_INDEX8,
    width, height
);

this.gl.framebufferRenderbuffer(
    this.gl.FRAMEBUFFER, this.gl.STENCIL_ATTACHMENT,
    this.gl.RENDERBUFFER, this.stencilBuffer
);

この仕組みを導入する事でGPUの負荷は結構軽減しました。
ただ、この仕組みの導入で別の問題が発生しました。

コール数が多い

WebGLの速度関連で検索するとよく見かける言葉です・・・

createしたbuffer情報を使い回せていない

  • createTexture
  • createRenderbuffer
  • createBuffer

などなど
作ったbuffer情報が不要になったら破棄していたのですが
そもそもcreate系の関数のコール負荷が非常に高かったです。

なので、使い終わったbufferをpoolして使い回す事でGPUへの負荷を軽減しました。

また、次に利用するbufferのサイズが一致していれば
さらに余計な処理を省く事ができます。

/**
 * @param  {number} width
 * @param  {number} height
 * @return {WebGLRenderbuffer}
 * @public
 */ 
getStencilBuffer (width, height)
{
    for (let idx = 0; idx < this.objectPool.length; idx++) {
        const stencilBuffer = this.objectPool[idx];
        if (stencilBuffer.width === width && stencilBuffer.height === height) {
            this.objectPool.splice(idx, 1);
            return stencilBuffer;
        }
    }

    const stencilBuffer  = this.gl.createRenderbuffer();
    stencilBuffer.width  = 0;
    stencilBuffer.height = 0;
    return stencilBuffer;
}

/**
 * @param  {number} width
 * @param  {number} height
 * @return {WebGLRenderbuffer}
 * @public
 */ 
create (width, height)
{
    const stencilBuffer = this.getStencilBuffer(width, height);

    if (stencilBuffer.width !== width || stencilBuffer.height !== height) {
        stencilBuffer.width  = width;
        stencilBuffer.height = height;

        this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, stencilBuffer);
        this.gl.renderbufferStorage(
            this.gl.RENDERBUFFER,
            this.gl.STENCIL_INDEX8,
            width, height
        );
    }

    return stencilBuffer;
}

この例だと

  • bindRenderbuffer
  • renderbufferStorage

が省略されます。
コール数が減るとGPUの負荷がかなり軽減されます。

必要最低限の描画領域に対して描画を行えていない

scissorなどで描画に必要な領域だけに対して描画したりclearする事でGPUの負担を減らす事ができました。

/**
 * @param  {number} x
 * @param  {number} y
 * @param  {number} w
 * @param  {number} h
 * @retuen {void}
 * @public
 */
clear (x, y, w, h)
{
    this.gl.enable(this.gl.SCISSOR_TEST);
    this.gl.scissor(x, y, w, h);
    this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.STENCIL_BUFFER_BIT);
    this.gl.disable(this.gl.SCISSOR_TEST);
}

まだまだ、やれる事はいっぱいあると思うのですが
大きな改善が見れたポイントを書きました。

明日は、テキスト入力をどうやって再現したかを書こうと思います。

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

東京五輪開催で夏の祝日が移動 〜各言語の祝日ライブラリの2021年の祝日対応を追ってみる〜

大遅刻をしてしまいましたが、これはミクシィグループ Advent Calendar 2020 12日目の記事です。

以前、2018年の年末に「#平成最後 のクリスマスに贈る、2019年の祝日対応と改元の話」という記事を書いたことがあります。この記事を書いたときは、祝日について考えることなんて当分ないだろうと考えていたんですが、2年後の今年、また祝日について考えなければならないようです……。

COVID-19による東京オリンピック 2020の延期

みなさんご存知のとおり、2020年の年初から世界中で新型コロナウイルス COVID-19が猛威をふるい、2020年の夏に東京で開催予定だった 東京オリンピック2020も2021年に延期となりました。

日本では、2020年のオリンピックの開催に合わせ、法律を改正(平成三十二年東京オリンピック競技大会・東京パラリンピック競技大会特別措置法及び平成三十一年ラグビーワールドカップ大会特別措置法の一部を改正する法律)し一部の祝日を移動し開会式と閉会式前後を連休にして、通勤・通学者を減らし、混乱を和らげるようにしていました。
東京オリンピック 2020が2021年に延期されたことで、同じように2021年の祝日も一部移動させ、開会式と閉会式前後を連休にしようという案が2020年5月29日に内閣府から提出されていました(平成三十二年東京オリンピック競技大会・東京パラリンピック競技大会特別措置法等の一部を改正する法律案)。
2020年11月27日に衆参両院でその案が可決され、平成三十二年東京オリンピック競技大会・東京パラリンピック競技大会特別措置法
等の一部を改正する法律
として、2020年12月4日に公布されました。

2021年の祝日

平成三十二年東京オリンピック競技大会・東京パラリンピック競技大会特別措置法
等の一部を改正する法律
で変更される祝日をまとめると、以下のようになります。

祝日名 本来の日付 2021年
海の日 7月 第3月曜日 7月22日 木曜日
スポーツの日 10月 第2月曜日 7月23日 金曜日
山の日 8月11日 8月8日 日曜日
山の日 振替休日 - 山の日が日曜日なのでその振替休日

ちょっと分かりづらいのでカレンダーにしてみます。

2021年の祝日

祝日ライブラリの対応状況

祝日を自分で判定するのは骨が折れるので、たいていの場合はライブラリを利用すると思います。
2021年の祝日について、各言語の祝日判定機能を提供してくれるライブラリの対応状況を見ていきたいと思います。

日本の祝日データセット holiday-jp/holiday_jp

まず、日本の祝日のデータセットを提供している holiday-jp/holiday_jp について確認しておきます。

Holiday accompanying coronation day #101」という、前述した2021年の祝日をデータセットに追加するプルリクが11月26日に出されています。
祝日についての法案が可決されたのが11月27日なので、毎度とても早い初動ですね。
実際の公布・施行を待ち、テストなどの修正をしたのち、12月2日にmasterへマージされています。

これから紹介するいくつかのライブラリはこのデータセットを利用しています。
しかしながら、このデータセットの更新を取り込んでいるかどうかはライブラリ次第なので、それぞれ確認が必要です。

Perl

Calendar::Japanese::Holiday : 対応済み(2020/12/11)

2020年12月11日に2021年の祝日に対応した バージョン 0.07 がリリースされました。

Ruby

holiday_jp : 対応済み(2020/12/09)

バージョン 0.8.0 で2021年の祝日に対応しました。

date-holiday : 未対応(2020/12/16 現在)

現在のバージョンは 2018/12/14 にリリースされた 0.0.5 で、2021年の祝日には対応していません。
0.0.5 のリリースの際に、2019年、2020年の祝日への対応をしていたので、一応アクティブな gem ではあるはずです。

holidays : 未対応(2020/12/16 現在)

全世界の祝日を集め、判定を行うライブラリです。現時点での最新のバージョンは 8.3.0 です。

これも、ライブラリとは別にデータセットを提供しているリポジトリが別に存在しています。
データセットも現時点では2021年の祝日には対応しておらず、2021年の祝日を追加する Add 2021 jp holiday という PR が GitHub 上で出されています。

holiday_japan : 対応済み(2020/11/27)

内閣府から改正案が出せれた 5/29 に move Marine day, Sports day, Mountain day for 2021 Olympic という commit が master ブランチへ push されていて、最終的に改正案が可決された 11/27 に 1.4.4 として RubyGems へリリースされています。
祝日ライブラリ界では最速の対応と言っていいかもしれません。

holiday : 未対応(2020/12/16 現在)

指定の形式のYAMLファイルを用意し、読み込ませることで祝日かどうかを判定できるライブラリのようです。
初回リリースの2011年9月からメンテナンスもされていないので、利用は推奨しません。

JavaScript

@holiday-jp/holiday_jp : 対応済み(2020/12/06)

holiday_jp データセットを利用しており、最新版のデータを取り込んだ差分が、v2.3.0 としてリリースされています。

PHP

holiday_jp : 対応済み(2020/12/06)

holiday_jp データセットを利用しており、最新版のデータを取り込んだ差分が、v2.3.0 としてリリースされています。

Java

holidayjp(holiday_jp) : 未対応(2020/12/16 現在)

holiday_jp データセットを利用していますが、最新版の取り込みはまだされていません。
現在の最新のバージョンは、2.0.1 です。

Swift

HolidayJp(holiday_jp) : 未対応(2020/12/16 現在)

holiday_jp データセットを利用していますが、最新版の取り込みはまだされていません。
最新のバージョンは、0.2.1 です。

Go

flagday : 対応済み(2020/09/01)

09/01 に Add holiday definitions in 2021 with postponement of the Olympics という PR で対応されていました。

holiday_jp : 未対応(2020/12/16 現在)

holiday_jp データセットを利用していますが、最新版の取り込みはまだされていません。
参照している holiday_jp データセットも、2019年にあった天皇の退位・即位による4月から5月にかけての10連休に対応していない少し古いものを参照しているようです。

Elixir

holiday_jp : 未対応(2020/12/16 現在)

holiday_jp データセットを利用していますが、最新版の取り込みはまだされていません。
現在の最新のバージョンは、0.3.6 です。

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

[Vue.js]ref属性を使って子コンポーネントインスタンスと子要素へのアクセスする

はじめに

refを初めて使用したので、備忘録として残します

ref属性とは

プロパティとイベントが存在するにも関わらず、ときどき JavaScript で直接子コンポーネントにアクセスする必要がある。
その場合に ref 属性を使うと、子コンポーネントにリファレンス ID を割り当てることができる

実装方法

今回はインプットフォームにフォーカスさせる

①インプットフォームにref属性を付与する

<input ref="input">

②親によって使用されるメソッドを定義して親コンポーネントに 内部の input 要素にフォーカスさせる

methods: {
  // 親からインプット要素をフォーカスするために使われる
  focus: function () {
    this.$refs.input.focus()
  }
}

これらの実装でfocusメソッドが動いた時にrefを付与したインプットフォームにフォーカスされる

参考

https://jp.vuejs.org/v2/guide/components-edge-cases.html#%E5%AD%90%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%81%A8%E5%AD%90%E8%A6%81%E7%B4%A0%E3%81%B8%E3%81%AE%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9

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

React チュートリアル

はじめに

Reactチュートリアルの追加課題の解答と、私が解いていく中で詰まったところを記述する。ReactやJSに関する知識が拙い中解いたので、より良い解き方などがあると考えられるが、一例として参考にしていただきたい。(より良い解き方など指摘いただければ追記致します。)
コードはチュートリアル内の最終結果を改変した。

1.履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。

考え方

着手の位置を履歴として残すために、historyに新たな要素として追加する。その後movesに着手の位置も表示するように変更すればよい。

解答例

まず、Gameクラスのコンストラクタにあるhistoryと盤面が動いた際の処理が書かれたメソッドhandleClick内のhistoryputStateをkeyとした着手の位置を保存するための要素を追加する(history自体ではなくhistory内にある辞書やhistoryに追加する辞書に対しての意)。
コンストラクタでは以下のように変更する(ここで定義しなくても動く)。

history: [
    {
        squares: Array(9).fill(null),
        putState: "(0, 0)"
    }
]

だが、handleClickメソッドでは着手の位置を与える必要がある。幸運なことに(当然だが)このメソッドは選択したマスの番号を引数として与えられているので、以下のように変更するだけでよい。

history: history.concat([
    {
        squares: squares,
        putState: ` (${i/3|0}, ${i%3})`
    }
])

JavaScriptのバージョンによっては文字列の埋め込みがこのようにできないので以下のようにする。

history: history.concat([
    {
        squares: squares,
        putState: " (" + (i/3|0).toString() + ", " + (i%3).toString() + ")"
    }
])

このようにして着手の位置を履歴として残せるようになったので最後にmovesを変更し、着手の位置を表示させる。

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

2.着手履歴のリスト中で現在選択されているアイテムをボールドにする。

考え方

選択されている盤面はGameクラスのメンバ変数StepNumberに格納されている。また、着手履歴の表示に関する部分はmovesに格納されている。このmovesは着手履歴を保存しているhistoryを用いてrenderされる度に生成される。さらに表示したい盤面を着手履歴のリストから選択するたびStepNumberが更新され再度renderされる。つまり、選択のたびrenderされるのでmovesが生成されるときにStepNumberと一致する番目にあるアイテムをボールドするようにすれば良いと考えられる。

解答例

変更点はmovesの定義部分のみ

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

最初falseの時のdesc{desc}と書いていてerror({}が二重になるため)を出していたのでこの部分は注意したい。

3.Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。

考え方

単純にリストを作って二重ループ回してリストにJSXを適宜格納し、ハードコーディング部分をリストに置き換える。

解答例

考え方通りコードを書く。

render() {
    const squareBoard = [];
    const row = 3;
    const col = 3;
    for(let i=0; i<row; i++){
        let rowBoard = [];
        for(let j=0;j<col; j++){
            rowBoard.push(this.renderSquare(j+3*i));
        }
        squareBoard.push(
            <div className="board-row">
                {rowBoard}
            </div>
        )
    }
    return (
        <div>
            {squareBoard}
        </div>
    );
}

これで上手くいくと考えていたが警告が出る。
Warning: Each child in a list should have a unique "key" prop.
これはkeyに関する警告でチュートリアル中にも触れられていたものである。keyを設定することでVirtualDOMのdiffから実際のDOMに反映させるときの変更を最小限にすることができる。変更を施すと以下のようになる。

class Board extends React.Component {
    renderSquare(i) {
        return (
            <Square
                value={this.props.squares[i]}
                onClick={() => this.props.onClick(i)}
                key={"square-"+i.toString()}
            />
        );
    }

    render() {
        const squareBoard = [];
        const row = 3;
        const col = 3;
        for(let i=0; i<row; i++){
            let rowBoard = [];
            for(let j=0;j<col; j++){
                rowBoard.push(this.renderSquare(j+3*i));
            }
            squareBoard.push(
                <div className="board-row" key={"row-"+i.toString()}>
                    {rowBoard}
                </div>
            )
        }
        return (
            <div>
                {squareBoard}
            </div>
        );
    }
}

ここでは"row-"+i.toString()としているがiだけでも問題ない(Squareも同様)。

4.着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。

私はトグルボタンそのものを実装することは本質ではなく、コードが複雑になると考えたので単純にボタンで実装した(トグルボタンにしたい方はトグルボタンのコンポーネントを作成してボタンのタグをトグルボタンのタグに変更することでできる)。

考え方

着手履歴は、Gameクラスのメンバ変数historyに格納されており、movesによって実装されている。moveshistoryから履歴を一手ずつ取ってきてJSXにしたものをリストにしている。historyは常に昇順であるためこれから作られたmovesもまた常に昇順となっている。つまり降順で表示したいときはmovesを逆順にすると良いと考えられる。

解答例

昇順、降順ボタンを任意の位置(ここでは着手履歴の上)に設定し、それを押すことで新たに追加するGameのメンバ変数ascendingの値を変える。ascendingは昇順でtrue、降順でfalseとなるようにする。ascendingの値によってrenderの直前にmovesを逆順にするかどうかを決める。

constructor(props) {
    super(props);
    this.state = {
        history: [
            {
                squares: Array(9).fill(null)
            }
        ],
        stepNumber: 0,
        xIsNext: true,
        ascending: true,
    };
}
render() {
    const history = this.state.history;
    const current = history[this.state.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.jumpTo(move)}>{desc}</button>
            </li>
        );
    });

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

    if(!this.state.ascending) moves.reverse();

    return (
        <div className="game">
            <div className="game-board">
                <Board
                    squares={current.squares}
                    onClick={i => this.handleClick(i)}
                />
            </div>
            <div className="game-info">
                <div>{status}</div>
                <button onClick={()=>this.setState({ascending: true})}>昇順</button>
                <button onClick={()=>this.setState({ascending: false})}>降順</button>
                <ol>{moves}</ol>
            </div>
        </div>
    );
}

一つ目はGameクラスのコンストラクタ部分で、二つ目はGameクラスのrender部分である。トグルボタンでは押したときにascendingをtrueやfalseを切り替えるように実装してやれば良い。

<ToggleButton onClick={()=>this.setState({ascending: !ascending})}/>

5.どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。

考え方

勝敗が決定したときにハイライトさせるので勝敗を確認するcalculateWinner関数と勝敗によって値が変わるGameクラス内のstatusを定義している部分をうまく変更すれば良いと考えられる。まず勝敗につながった3マスをどのように抜き出すかを考える。calculateWinner関数では勝利につながる可能性のある3マスの組を全列挙しそのうちの一つでも3マス全てが同じ記号であればその記号を返し、1つもなければnullを返している。つまり、calculateWinner関数では勝敗を決まった時勝利につながった3マスを特定することができ、返り値にその3マスも返すようにすることが可能であると考えられる。次に抜き出した情報をもとにハイライトを行う方法を考える。勝敗が決まった時のみこの処理を行うのでハイライトを行う機能はstatusを定義する場面で行う。勝敗が決まった時の処理でcurrent.squaresの先ほど抜き出した3マスをハイライトする機能を追加すると良いと考えられる。

解答例

まず、clulculateWinner関数の返り値を既存のものと勝利につながった3マスを合わせたものへと変更する。

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], lines[i]];
        }
    }
    return [null, []];
}

返り値を変えたことによってhandleClickメソッドに少々の変更を行う。

handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares)[0] || squares[i]) {//ここだよ
        return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
        history: history.concat([
            {
                squares: squares
            }
        ]),
        stepNumber: history.length,
        xIsNext: !this.state.xIsNext
    });
}

最後にstatus内の処理に一手間加える。

let status;
if (winner[0]) {
    status = "Winner: " + winner[0];
    for(let i in winner[1]) current.squares[winner[1][i]] = <font color="#F00">{current.squares[winner[1][i]]}</font>;
} else {
    status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}

ハイライトは3つのマスを赤色にすることにした(これは何でもいい)。
しかし、この方法だと勝利につながったマスが5つの時であっても3つしかハイライトされない。このようなケースは通常人間同士が真剣に行ったときに出ることはないが、気持ち悪いので5つともハイライトされるようにする。変更したのはcalculateWinner関数のみ。

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]
    ];
    let winner = null;
    let winLines = [];
    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]) {
            winLines = winLines.concat(lines[i]);
            winner = squares[a];
        }
    }
    return [winner, winLines];
}

6.どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。

考え方

引き分けのメッセージはstatusに新たな分岐を作ることによって、引き分けの判別はcalculateWinner関数で引き換えの処理を加えればよいと考えられる。また、引き分けは勝敗が決まってない時のsquaresの中身にnullがあるかないかで判別することができる。

解答例

まず、statuscalculateWinner関数の返り値に依存しているので、calculateWinner関数を変更する。この関数はhandleClickメソッドにも使われており、それに影響が出ないような形で変更したい。つまり、bool値がtrueとなるような値になるようにする。

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];
        }
    }

    if (squares.indexOf(null) === -1) return "draw";

    return null;
}

最後にstatusの定義部分を変更する。引き分けになった時の表示はDrawにした。

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

おわりに(全体コード)

追加課題を全て終えた後はこのようなコードになった。より良い書き方や書き方の問題などがあれば追記するので教えていただきたい。

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

Reactのチュートリアル追加課題解いてみた

はじめに

Reactチュートリアルの追加課題の解答と、私が解いていく中で詰まったところを記述する。ReactやJSに関する知識が拙い中解いたので、より良い解き方などがあると考えられるが、一例として参考にしていただきたい。(より良い解き方など指摘いただければ追記致します。)
コードはチュートリアル内の最終結果を改変した。

1.履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。

考え方

着手の位置を履歴として残すために、historyに新たな要素として追加する。その後movesに着手の位置も表示するように変更すればよい。

解答例

まず、Gameクラスのコンストラクタにあるhistoryと盤面が動いた際の処理が書かれたメソッドhandleClick内のhistoryputStateをkeyとした着手の位置を保存するための要素を追加する(history自体ではなくhistory内にある辞書やhistoryに追加する辞書に対しての意)。
コンストラクタでは以下のように変更する(ここで定義しなくても動く)。

history: [
    {
        squares: Array(9).fill(null),
        putState: "(0, 0)"
    }
]

だが、handleClickメソッドでは着手の位置を与える必要がある。幸運なことに(当然だが)このメソッドは選択したマスの番号を引数として与えられているので、以下のように変更するだけでよい。

history: history.concat([
    {
        squares: squares,
        putState: ` (${i/3|0}, ${i%3})`
    }
])

JavaScriptのバージョンによっては文字列の埋め込みがこのようにできないので以下のようにする。

history: history.concat([
    {
        squares: squares,
        putState: " (" + (i/3|0).toString() + ", " + (i%3).toString() + ")"
    }
])

このようにして着手の位置を履歴として残せるようになったので最後にmovesを変更し、着手の位置を表示させる。

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

2.着手履歴のリスト中で現在選択されているアイテムをボールドにする。

考え方

選択されている盤面はGameクラスのメンバ変数StepNumberに格納されている。また、着手履歴の表示に関する部分はmovesに格納されている。このmovesは着手履歴を保存しているhistoryを用いてrenderされる度に生成される。さらに表示したい盤面を着手履歴のリストから選択するたびStepNumberが更新され再度renderされる。つまり、選択のたびrenderされるのでmovesが生成されるときにStepNumberと一致する番目にあるアイテムをボールドするようにすれば良いと考えられる。

解答例

変更点はmovesの定義部分のみ

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

最初falseの時のdesc{desc}と書いていてerror({}が二重になるため)を出していたのでこの部分は注意したい。

3.Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。

考え方

単純にリストを作って二重ループ回してリストにJSXを適宜格納し、ハードコーディング部分をリストに置き換える。

解答例

考え方通りコードを書く。

render() {
    const squareBoard = [];
    const row = 3;
    const col = 3;
    for(let i=0; i<row; i++){
        let rowBoard = [];
        for(let j=0;j<col; j++){
            rowBoard.push(this.renderSquare(j+3*i));
        }
        squareBoard.push(
            <div className="board-row">
                {rowBoard}
            </div>
        )
    }
    return (
        <div>
            {squareBoard}
        </div>
    );
}

これで上手くいくと考えていたが警告が出る。
Warning: Each child in a list should have a unique "key" prop.
これはkeyに関する警告でチュートリアル中にも触れられていたものである。keyを設定することでVirtualDOMのdiffから実際のDOMに反映させるときの変更を最小限にすることができる。変更を施すと以下のようになる。

class Board extends React.Component {
    renderSquare(i) {
        return (
            <Square
                value={this.props.squares[i]}
                onClick={() => this.props.onClick(i)}
                key={"square-"+i.toString()}
            />
        );
    }

    render() {
        const squareBoard = [];
        const row = 3;
        const col = 3;
        for(let i=0; i<row; i++){
            let rowBoard = [];
            for(let j=0;j<col; j++){
                rowBoard.push(this.renderSquare(j+3*i));
            }
            squareBoard.push(
                <div className="board-row" key={"row-"+i.toString()}>
                    {rowBoard}
                </div>
            )
        }
        return (
            <div>
                {squareBoard}
            </div>
        );
    }
}

ここでは"row-"+i.toString()としているがiだけでも問題ない(Squareも同様)。

4.着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。

私はトグルボタンそのものを実装することは本質ではなく、コードが複雑になると考えたので単純にボタンで実装した(トグルボタンにしたい方はトグルボタンのコンポーネントを作成してボタンのタグをトグルボタンのタグに変更することでできる)。

考え方

着手履歴は、Gameクラスのメンバ変数historyに格納されており、movesによって実装されている。moveshistoryから履歴を一手ずつ取ってきてJSXにしたものをリストにしている。historyは常に昇順であるためこれから作られたmovesもまた常に昇順となっている。つまり降順で表示したいときはmovesを逆順にすると良いと考えられる。

解答例

昇順、降順ボタンを任意の位置(ここでは着手履歴の上)に設定し、それを押すことで新たに追加するGameのメンバ変数ascendingの値を変える。ascendingは昇順でtrue、降順でfalseとなるようにする。ascendingの値によってrenderの直前にmovesを逆順にするかどうかを決める。

constructor(props) {
    super(props);
    this.state = {
        history: [
            {
                squares: Array(9).fill(null)
            }
        ],
        stepNumber: 0,
        xIsNext: true,
        ascending: true,
    };
}
render() {
    const history = this.state.history;
    const current = history[this.state.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.jumpTo(move)}>{desc}</button>
            </li>
        );
    });

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

    if(!this.state.ascending) moves.reverse();

    return (
        <div className="game">
            <div className="game-board">
                <Board
                    squares={current.squares}
                    onClick={i => this.handleClick(i)}
                />
            </div>
            <div className="game-info">
                <div>{status}</div>
                <button onClick={()=>this.setState({ascending: true})}>昇順</button>
                <button onClick={()=>this.setState({ascending: false})}>降順</button>
                <ol>{moves}</ol>
            </div>
        </div>
    );
}

一つ目はGameクラスのコンストラクタ部分で、二つ目はGameクラスのrender部分である。トグルボタンでは押したときにascendingをtrueやfalseを切り替えるように実装してやれば良い。

<ToggleButton onClick={()=>this.setState({ascending: !ascending})}/>

5.どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。

考え方

勝敗が決定したときにハイライトさせるので勝敗を確認するcalculateWinner関数と勝敗によって値が変わるGameクラス内のstatusを定義している部分をうまく変更すれば良いと考えられる。まず勝敗につながった3マスをどのように抜き出すかを考える。calculateWinner関数では勝利につながる可能性のある3マスの組を全列挙しそのうちの一つでも3マス全てが同じ記号であればその記号を返し、1つもなければnullを返している。つまり、calculateWinner関数では勝敗を決まった時勝利につながった3マスを特定することができ、返り値にその3マスも返すようにすることが可能であると考えられる。次に抜き出した情報をもとにハイライトを行う方法を考える。勝敗が決まった時のみこの処理を行うのでハイライトを行う機能はstatusを定義する場面で行う。勝敗が決まった時の処理でcurrent.squaresの先ほど抜き出した3マスをハイライトする機能を追加すると良いと考えられる。

解答例

まず、clulculateWinner関数の返り値を既存のものと勝利につながった3マスを合わせたものへと変更する。

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], lines[i]];
        }
    }
    return [null, []];
}

返り値を変えたことによってhandleClickメソッドに少々の変更を行う。

handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares)[0] || squares[i]) {//ここだよ
        return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
        history: history.concat([
            {
                squares: squares
            }
        ]),
        stepNumber: history.length,
        xIsNext: !this.state.xIsNext
    });
}

最後にstatus内の処理に一手間加える。

let status;
if (winner[0]) {
    status = "Winner: " + winner[0];
    for(let i in winner[1]) current.squares[winner[1][i]] = <font color="#F00">{current.squares[winner[1][i]]}</font>;
} else {
    status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}

ハイライトは3つのマスを赤色にすることにした(これは何でもいい)。
しかし、この方法だと勝利につながったマスが5つの時であっても3つしかハイライトされない。このようなケースは通常人間同士が真剣に行ったときに出ることはないが、気持ち悪いので5つともハイライトされるようにする。変更したのはcalculateWinner関数のみ。

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]
    ];
    let winner = null;
    let winLines = [];
    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]) {
            winLines = winLines.concat(lines[i]);
            winner = squares[a];
        }
    }
    return [winner, winLines];
}

6.どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。

考え方

引き分けのメッセージはstatusに新たな分岐を作ることによって、引き分けの判別はcalculateWinner関数で引き換えの処理を加えればよいと考えられる。また、引き分けは勝敗が決まってない時のsquaresの中身にnullがあるかないかで判別することができる。

解答例

まず、statuscalculateWinner関数の返り値に依存しているので、calculateWinner関数を変更する。この関数はhandleClickメソッドにも使われており、それに影響が出ないような形で変更したい。つまり、bool値がtrueとなるような値になるようにする。

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];
        }
    }

    if (squares.indexOf(null) === -1) return "draw";

    return null;
}

最後にstatusの定義部分を変更する。引き分けになった時の表示はDrawにした。

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

おわりに(全体コード)

追加課題を全て終えた後はこのようなコードになった。より良い書き方や書き方の問題などがあれば追記するので教えていただきたい。

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

フロントエンドだけで可能!?Chrome拡張に形態素解析を組み込む

はじめに

Chrome拡張を作っている時にユーザが入力する任意の短文を適切に品詞分解したくなりました。

例えば「すもももももももものうち」という有名な短文があるがこれを品詞に分解すると
「すもも」「もも」などの名詞や「も」という助詞に分けることができます。

fruit_soldum.png

これを機械的に実現する方法として「形態素解析」が広く用いられていますが、Chrome拡張で実現するためにフロントだけでどうにかする方法は無いかと探していたらkuromoji.jsというライブラリを見つけました。

kuromoji.js
https://github.com/takuyaa/kuromoji.js/

kuromoji
https://www.atilika.com/ja/kuromoji/

今回は、Chrome拡張への組み込みに少し苦労したので
誰でも簡単に組み込んでデモできるようになるまでの流れを作りたいと思います。

準備

Chrome拡張を作るためのベースの知識がある前提で話しますが、手順にそうと誰でも拡張機能が作れます。
またストアにアップロードしなくてもローカルで試せますのでそのような手順になってます。

ディレクトリ

下記の構成で準備します

extension
    ├── manifest.json
    └── script.js

ベースファイル

manifest.js
{
  "manifest_version": 2,
  "name": "拡張機能の名前",
  "description": "拡張機能の説明",
  "version": "0.0.1",
  "permissions": [
    "*://example.com/*"
  ],
  "content_scripts": [
    {
      "matches": ["*://example.com/*"],
      "js": [
        "kuromoji/build/kuromoji.js",
        "script.js"
      ]
    }
  ],
  "web_accessible_resources": ["/kuromoji/dict/*"]
}

マニフェストファイルは拡張の構成を定義するファイルです。
- manifest_version (定義ファイルの形式のバージョン)
- name (拡張の名前)
- description (拡張の説明)
- version (拡張のバージョン)
- permissions (拡張機能の実行のために求める権限、ブラウザの機能や特定のページに対して行う)
- content_scripts (WEBページで実行するJavaScriptの指定、特定のページに対して実行することもできる)
- web_accessible_resources (拡張機能がアクセスできるアセットの指定)

設定のポイントは
- https://example.com というサイトで実行するスクリプトを想定
- script.jsにメインの処理
- 形態素解析のために必要な辞書をweb_accessible_resourcesで許可する
ところです

script.js
// 辞書の場所を指定 extensionAPIを利用して相対的にパスを指定する
const dicPath = chrome.extension.getURL("/kuromoji/dict/")

kuromoji.builder({ dicPath: dicPath }).build(function (err, tokenizer) {
    // tokenizer is ready
    var path = tokenizer.tokenize("すもももももももものうち");
    console.log(path);
});

メインの処理はここに記載します。
kuroomoji.jsのサンプル通りに特定の文書を解析して結果をコンソールログで確認したいと思います。

kuromojijsの導入

拡張のディレクトリの中にとりあえずcloneしてきます。
そのままだとディレクトリ名に「.」が入るので「kuromoji」と改めて定義しています。

cd extension
git clone https://github.com/takuyaa/kuromoji.js.git kuromoji

現状のファイル構成はこうなります。
manifestやscriptを記述する際にkuromoji以下のディレクトリがある事を前提に記述していたので、疑問に思ったと思う箇所もあると思いますが見比べてみるとわかると思います。

extension
    ├── kuromoji
    │   ├── CHANGELOG.md
    │   ├── LICENSE-2.0.txt
    │   ├── NOTICE.md
    │   ├── README.md
    │   ├── bower.json
    │   ├── build
    │   ├── demo
    │   ├── dict
    │   ├── example
    │   ├── gulpfile.js
    │   ├── jsdoc.json
    │   ├── package-lock.json
    │   ├── package.json
    │   ├── src
    │   └── test
    ├── manifest.json
    └── script.js

拡張機能をChromeに

拡張機能はストアに公開しなくてもローカルファイルを読み込むことで動作の確認ができます

Chromeブラウザで chrome://extensions/ を開きます。

「パッケージ化されていない拡張機能を読み込む」ボタンをクリックして、先程作成した「extension」ディレクトリを選択します

この時、問題がある場合はパッケージが読み込めずにエラー文が表示されます。
エラー内容を読むと例えばmanifest.jsonの記述に問題があるなどがわかるので該当箇所の作成手順に問題ないか見直します。

晴れて読み込まれたら

https://example.com を開きます

Developer toolを開いてコンソールに形態素解析されたワードが配列で表示されるはずです。

(7) [{…}, {…}, {…}, {…}, {…}, {…}, {…}]
0: {word_id: 415760, word_type: "KNOWN", word_position: 1, surface_form: "すもも", pos: "名詞", …}
1: {word_id: 93220, word_type: "KNOWN", word_position: 4, surface_form: "も", pos: "助詞", …}
2: {word_id: 1614710, word_type: "KNOWN", word_position: 5, surface_form: "もも", pos: "名詞", …}
3: {word_id: 93220, word_type: "KNOWN", word_position: 7, surface_form: "も", pos: "助詞", …}
4: {word_id: 1614710, word_type: "KNOWN", word_position: 8, surface_form: "もも", pos: "名詞", …}
5: {word_id: 93100, word_type: "KNOWN", word_position: 10, surface_form: "の", pos: "助詞", …}
6: {word_id: 62510, word_type: "KNOWN", word_position: 11, surface_form: "うち", pos: "名詞", …}
length: 7
__proto__: Array(0)

以上で形態素解析を用いたChrome拡張のベースができあがりました。

Chrome拡張からサーバーと通信すれば形態素解析ももっと自由度が高く実現できるとは思いますが
ブラウザのリソースだけで処理が完結する事は理想的だと思います。

今回はここまでですが
- WEBページから抽出した文字を解析する (名詞のみを取り出すなど)
- 文字にふりがな、よみがなを振る
などの展望が見れるので活用できたらいいなと思っています。

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

Flash Advent Calendar 11日目 - WebGL導入への道のり04 中抜き多角形 -

WebGLを導入するにあたり描画課題として

  • グラデーション
  • マスク
  • 中抜き描画
  • 線の描画
  • ブレンド
  • フィルター
  • 9-slice

っと、まだまだ書けそうな内容があったので
負荷部分の前に、もう少しだけ描画関連の事を書こうと思います。

今日は中抜き描画に関して書こうと思います。
5日目の記事でも書いたのですが
swfの描画は一筆書きで、時計回りと半時計周りが重なる部分は描画しないっという仕様です。

最初にチャレンジしてみた事

パスの外周から三角形のポリゴン情報を自前で作ってみる

中抜き描画するのに、最初に思いついたのは自前で三角形(ポリゴン)を作ってみる
って事でした。
処理内容が難しすぎて完成には至りませんでした。。。

三角形のポリゴン情報を自前で作ってみる

外周のパス情報から三角形を刈り取っていく
0,0の起点から一番遠い箇所から刈り取っていくのが一般的だそうです。

sample.png

起点となる座標のベクトルを維持したまま次の座標に移動します。
そうしないと、内側と外側の判断が行われず、変なポリゴンができてしまいます。
ご注意を
sample2.png

ここまではなんとか実装できたのですが、次で挫けてしまいました。

穴あき多角形の三角形(ポリゴン)

完成したい描画
sample3.png

まず始めに多角形を結合して一個の多角形にします。
切れ目となる座標と同じx,yをもう一個追加して、データ的にも結合します。

sample4.png

後は、上の刈り取り方法で同じように刈り取っていきます。
sample5.png

比較的、色々な多角形に対応できたのですが
自分の技量では複雑な多角形には対応できなかったのでこの手法は断念してしまいました。
また、頂点情報が肥大化して描画負荷が高くなってしまいました。

色々と試したのですが、中抜き多角形の塗り描画は
この記事の手法で描画する事で全ての問題が解決しました。

刈り取り手法を使えば、線の幅を自由に可変できる描画は実現可能だったので
結果的には無駄ではなかったのですが、色々と力量不足を感じました。
日々精進です・・・

次回こそはWebGLの負荷に関して書こうと思います。

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

JavaScriptとPHPを習い始めたばかりの人間がECサイトを構築するトライアル開始します

こんにちは。

プログラミング初学者ですが、
授業でJavaScriptとPHP、APIやFirebaseを学んだので
何かしら形にしてアウトプットをしてみたくなりました。

年末年始は授業もなく時間もできるので、一旦どこまでかかるかすら
把握しておりませんが、ECサイトを作ることに挑戦しようと思います。

思い立った敬意としては、学校のメンターでありCTOなど務めていらっしゃる
偉大な先輩から、

「作りたいものがあって、それをコード書きまくってダメなら何がダメか調べて
を繰り返し、自走していくことでようやく血肉となった結果自分の企画も
レベルが上がっていく」

なんとも筋トレのようなスポーツのような言葉をいただきとても共感させていただいたから。

過程やStuckしたポイントをどんどん更新しながらあげていって
同様にプログラム初学者の方々の背中を少しでも押せるような内容にしたいと思います。

日記のような形でこまめに更新していきます。

次の投稿は
・ECサイトにある機能のブレークダウン
・ECサイト作成に必要な技術

この辺り投稿予定。

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

【kintone】for-ofやforEachでテーブルの値を覗いてみる

こんにちは!
3~4年くらい前に「for」は時代遅れだ!
forじゃなくてmapメソッドを使いましょう!

なんてことを言い聞かされて、
プログラマーXX歳定年説みたいなのをなんとなく意識してしまったことのあるジュリドン(当時事務職)です。

今日は、そんなforの仲間、for-ofを紹介しつつ、
配列で使えるforEachメソッドも使って、
テーブルのフィールドの値を取得してみたいと思います。

for-ofとは

オブジェクトのなかから1つずつ値をとってきて処理できる繰り返し文です。
こんな感じで使います。

for(const x of オブジェクト){
   console.log(x);
}

for-ofの使い方1

突然ですが、for-ofを試しに使ってみたいと思います。
たとえばこんなオブジェクトがあったときを考えましょう・・・

const lunch = [
    {名前:"ひめ", セット:"A", デザート:"チョコ"},
    {名前:"たろう", セット:"B", デザート:"チョコ"},
    {名前:"なすび", セット:"C", デザート:"チョコ"}
];

↓こうすると配列の最初から最後までとってこれます

for(const r of lunch){
    console.log(r);
}

console上での実行結果↓
image.png

for-ofの使い方2

こうするとキーが「名前」のオブジェクトだけ取ってこれます。

for(const {名前} of lunch){
    console.log({名前});
}

console上での実行結果↓
image.png

for-ofの使い方3

こうすると名前の値だけ取ってこれます。
分割代入というやり方です。
参考:分割代入

for(const {名前:v} of lunch){
    console.log(v);
}

console上での実行結果↓
image.png

forEachで書き換えてみる

配列から1つずつ値を持ってきて処理する・・・・というメソッドですが、こんなふうに書きます。

配列.forEach(v =>{
    //処理
})

先程のfor-ofの使い方1~3を書き換えるとこんな感じ。結果も同じです。

//1
lunch.forEach(r => {
    console.log(r);
});

//2
lunch.forEach(({名前}) => {
    console.log({名前});
});

//3
lunch.forEach(({名前:v}) => {
    console.log(v);
});

テーブルで試す

テーブルの準備

こんなテーブルを準備しましょう。

テーブル名:ランチ

フィールド名/コード フィールド種類 内容
番号 数値
名前 文字列(1行)
セット ラジオボタン A B
デザート チェックボックス チョコ バニラ

JavaScriptを書く

こんな感じで、詳細画面で中身が確認できるように色々と書いてみました。
是非、実行してみてね✨✨
【kintone】テーブルの値を変更してみる(超初心者向け)等参考にしてくださいね。

即時関数の部分は自分で書いてね

//詳細画面
kintone.events.on(['app.record.detail.show'], event => {

    const record = event.record;
    console.log("record\n",record.ランチ.value);

    console.log("\n【for-of 1】for(const r of record.ランチ.value)");
    for(const r of record.ランチ.value){
        console.log(r);
    }

    console.log("\n【for-of 2】for(const {value} of record.ランチ.value)");
    for(const {value} of record.ランチ.value){
        console.log(value);
    }

    console.log("\n【for-of 3】for(const {value:r} of record.ランチ.value)");
    for(const {value:r} of record.ランチ.value){
        console.log(r);
    }

    console.log("\n【for-of 4】for(const {value:{名前:r}} of record.ランチ.value)");
    for(const {value:{名前:r}} of record.ランチ.value){
        console.log(r);
    }

    console.log("\n【for-of 5】for(const {value:{名前:{value:r}}} of record.ランチ.value)");
    for(const {value:{名前:{value:r}}} of record.ランチ.value){
        console.log(r);
    }

    console.log("\n【for-of 6】for(const r of record.ランチ.value)");
    for(const r of record.ランチ.value){
        console.log(r.value.名前.value);
    }

    console.log("\n【forEach 1】forEach");
    record.ランチ.value.forEach(({value:{名前:{value:r}}}) => {
        console.log(r);
    });

    console.log("\n【forEach 2】forEach");
    record.ランチ.value.forEach(r => {
        console.log(r.value.名前.value);
    });

    return event;
});    

実行結果のconsoleスクショ画像(長い)

image.png
image.png
image.png

まとめ

for-ofやforEachを使うと、自由自在?にテーブルのフィールドの値が取ってこれるような気がしてきました。
どの方法が一番いいのかは試していないですが、処理速度とか調べてみたいです。(調べたよって方がいたら教えて下さい)

というわけで、今日はこのへんで(・ω・)ノシノシ

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

javaScriptでwebpを使いたーい

はじめに

iOS14/macOSビッグサーのsafariがwebpに対応したということで、そろそろwebpを使って行こうかと思ったのですが、なんとjsからwebp使用判定ができず。

いやjsで使いたいんだもん。jsアプリを作ってるんだし。
なんできないねん。

acceptヘッダ? pictureタグ?
こんなこと言われてもね。

対応できた

なんとか対応できました。

非表示のimgタグにbase64にした小さいwebpを埋め込んで、エラーハンドリングして確認。
scriptタグはその下へ移動。

    <div style="visibility: hidden;">
        <img src="data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEACkC4JaQAA3AA/uZQAAA=" width="1" height="1"
            alt="" onload="webP=true;console.log('webp OK');" onerror="webP=false;console.log('webp NG');">
    </div>
    <script defer src="xxx.js"></script>

こうだね。
たまにタイミングが狂うので、onloadも取ってチェックした方がいい。

今回も手抜きな感じ:relaxed:

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

フロントエンド初心者が1年間で受けた指摘をまとめてみた【js編】

1年間にコードレビューで頂いたjsの指摘の中で、汎用性(?)の高そうなものをまとめてみました。

読みやすく保守しやすいコードの書き方は、(たぶん一番大事な部分なのに)独学では習得が難しいなと思いました。
受けた指摘も、読みやすいコードにするためにどう書くか?という指摘がメインです。

主観で★1(学習コスト0)〜★5(哲学)に分類してみてます。

もらった指摘

★1 学習コスト0

コミット前にconsole.logを消しておく

とはいえ、動作確認で必要になることも多いので、最悪レビュー前には消しておきたいくらいの気持ち。
複雑なコードを書いているときについつい消し忘れがち。
コミット前のgrepを癖付けると吉。

事情があってconsole.logを残したいとき(デモ環境などで確認が必要な場合)は、コミットメッセージに記載&レビュー用のプルリクエストにコメントとして残すと、レビュアーや未来の自分に優しいです。

不要なコードはコミットに含めない

超当たり前ではあるのですが、やりがち
①単純に作業の消し忘れ
②既存のコード準拠で新しくコードを書いていくとき
のパターンがありました。

①に関しては一旦作ってみたけど使わなくなった変数や関数が残ることが多かったです。
eslintなどで検出するといいかもしれません。

②は、既存コードをコメントアウトして残しておくと一見参考にできそうでいいように思えるのですが、
後々「どこを使ってて、どこを使ってないんだっけ…?」というのが分からなくなってしまうので、
利用しない・するかもしれない関数は最初からは入れず、段階に応じて持ってくるようにします。

コミット時点で使用していないものが混入してしまうと、読解コストが上がってしまうので気をつけたいですね…!(自戒)

改行・インデントが統一されていない

①改行に不要なスペースが入っている
②インデントがタブではなくスペースになっている
③改行が2行になっている

普通にド迷惑で恥ずかしい。けど初心者のうちはやらかしがち(だと思いたい。)
①と②は、VSCode拡張機能のindent-rainbowを入れると検知しやすさが爆上がりします。
③はeslintなどでゴリッと削ってしまいたいところですが、既存コードに追加など気軽にlintできないこともあるので、コミットを細切れにして、コミット前に確認すると良いかも。

★2 思いやり

スタイルに関するクラス名をjsからの参照に使わない

横着すると悲しみを背負います。

// 微妙なやつ

// html
<div class="header_title">ほげほげ</div>

// css
.header_title {
  font-size: 12px;
}

// js(jQuery)
$('.header_title').on('click', function() {
    ... //処理
  });

というコードにしてしまうと、

// 微妙なやつの成れの果て

// html
<div class="header_title_new">ほげほげ</div> // < スタイル変えるわ!

// css
.header_title_new { // < スタイル変えたわ!
  font-size: 12px;
}

// js(jQuery)
$('.header_title').on('click', function() { // < は?動かないんだが
    ... //処理
  });

という悲劇が起こるので、

// 良いやつ

// html
<div class="header_title js-header_title">ほげほげ</div>

// css
.header_title {
  font-size: 12px;
}

// js(jQuery)
$('.js-header_title').on('click', function() {
    ... //処理
});

分けると後々幸せになれます。

関数の引数を分かりやすく

// 微妙なやつ

const openTarget = (t, boolean) => {
  ... // 処理

↑よりは

// 良いやつ

const openTarget = ($target, isClosed) => {
  ... // 処理

こちらのほうが、引数の意味しているところが分かってちょっと読みやすくなります。

JSDocをちゃんと書く

TypeScriptを使えばそこそこ解決するのですが…。
引数としてどういう型を想定しているか?引数の内容はどんなもの(役割を持っている)か?などを書くと、未来の読解コストが下がります。

/**
 * [filterErrorCassette エラー状態のカセットのリストを返す]
 * @param {Object} $cassette 対象のカセット
 * @return {array} エラーカセットの配列
 */

varの巻き上げ(hoisting)に注意する

参考:やっとわかったjsの「巻き上げ」
なぜvarを使っているのかって?IEお前のせいだよ!

巻き上げ考慮して先に変数宣言するとき、ミスでglobalになってしまう可能性がある(varをつけずに宣言するとglobalになる)

const/letが使える人は使いましょう(涙)

constもテンプレートリテラルもIEでは動きません

上記varの巻き上げと同じく、constもテンプレートリテラルもIEでは使えないので注意ですね…。
constが使える環境からconst禁止環境に移ってくるとつい忘れがちです。

【jQuery】 $('.hoge')はコストが高いので複数回使う場合は変数宣言しておく

jQueryを高速に動作させるためのポイント5つ
クラス指定でDOMを取得する場合、jQuery内部では結構面倒なことをやっていたりします。
何度も同じDOMを使う場合は変数に入れておきましょう。
入社したての頃に一番言われたポイントかもしれないです。レビュアーさんありがとう…

★3 読みやすいコード

早期returnを使って関数をシンプルにする(ガード節)

// 微妙なやつ

if (hogehoge === true) {
  // 処理①
} else {
  // return;
}
  // 処理②

この書き方だと、こういう読み方になります

  • hogehogeがtrueのときに処理①を行って…
  • あ、それ以外は処理②やらないんだな

早期returnをすると…

// 良いやつ

if (hogehoge === false) {
  return;
}
  // 処理①
  // 処理②
  • なるほど、hogehogeがfalseのときは後の処理はしないんだな
  • falseでなければ処理①と処理②をするんだな

と、分岐を削れるため読むエネルギーが減ります!

!hogehoge.lengthよりhogehoge.length === 0のほうがよい

!を使うと(何の否定だろう…?)と「否定」であることを頭に置きながら読む必要があるので、単純に0やfalseなどと等価であることを示すほうが読みやすいです。

関数の起点となる(イベントをバインドする)クラスを付与する場合は、役割に応じた命名をする

// 微妙なやつ

// html
<button type="button" class="js-button">ポップアップ</button>

// js
$('.js-button').on('click', function() {
  ...

jsでclickなどイベントをバインドするクラス名は、上のように「もの」的なクラス名をつけるよりも…

// 良いやつ

// html
<button type="button" class="js-popup_open_trigger">ポップアップ</button>

// js
$('.js-popup_open_trigger').on('click', function() {
  ...

このように、「何をするのか」を元にクラス名をつけてあげると、クラスを見るだけで処理のイメージが付きやすくなってお得です。

可読性を高めるために変数を使用する

例えば、ウィンドウ幅が750px以下のときのみ実行したい関数があったときは
if (windowWidth < 750) { ...と書くよりも

const hasSmallerWindow = windowWidth < 750px;
if (hasSmallerWindow) {
  // 処理
}

とすると、750は狭いウィンドウ幅を判定するための数字なんだな…というのが分かりやすくなります。
とはいえ…

変数化しなくても可読性を損なわない場合もある

上で示した例は750の意味が単体では分からないので変数化したほうがよいのですが、
読む量が少なかったり、読んでちゃんと意味が分かるものについては変数化しなくても読みやすかったりするので、ケースバイケース(製作者のさじ加減…)です。

// 変数化してるけど…

const isInFixedAreaStatus = isScrollInRange(from);
togglePositionFixed({
    canDisplay: isInFixedAreaStatus,
    $target: $banner,
});

// 変数化しなくても読みやすい

togglePositionFixed({
  canDisplay: isScrollInRange(from),
    $target: $banner,
});

【jQuery】見えてるものだけ取りたい場合:visibleが使える

たまに出番があるかもしれません

$targetTrigger = $('.js-accordion_trigger:visible').length;

とすると、js-accordion_triggerクラスのうち、画面に表示されているDOMだけ取得できます。
他にも:checkedなど色々あります。

★4 職人

変数の置き場所を適切にする

繰り返し使用する関数内で変数を定義すると、呼び出し毎に変数が定義されることになります。
関数呼び出し前後で不変の値であれば、関数外で呼び出すとエコで気持ちがいいです。

// 微妙なやつ

$listItem.each(function() {
  const $document = $(document);
  const $this = $(this);
// 良いやつ

// documentは不変の値なので、eachの外で定義
const $document = $(document);

$listItem.each(function() {
  const $this = $(this);

スコープを限定させるために即時関数で囲う(ES5)

即時関数のメリットと主な用途
ES5にて、ちょっとしたjsをページに読み込むときなど、即時関数を使ってスコープを限定させ、グローバルから値が読まれてしまう可能性をなくしたりします。

ES6(ES2015)でグローバルに定義したlet,const,classはグローバルオブジェクトのプロパティにならない
ES6でletやconstが使える場合はグローバルにならないため、即時関数を使わなくても大丈夫です。

Safari系列ではdivタグのclickイベントが拾えない場合があるが、cursor:pointer;を付けると拾える

【JavaScript】Safariでclickイベントが発生しない?
謎現象ですが…。
ちなみにCSS側で-webkit-tap-highlight-color: transparent;を付与するとスマホでタップ時に出る灰色のハイライトが消えます。

関数内で基準値やセレクタなどを直接指定しない

// 基準値やセレクタを直接指定している

const filterContentValue = function() {
  const $target = $('.js-age_input');
  // 値が15以上のinputを抽出する
  const thresholdValue = 15;
  ...

上記のように基準値やセレクタを直接指定してしまうと

  • 覚えられない(関数を見に行かないと、どのDOMを参照しているのか?どの値から動作しているのか?が分からない)
  • 今はちゃんと動いていても、将来セレクタがページから削除されて動作しなくなる可能性が増える

など危険性が増しがちです。

// 基準値やセレクタを引数で渡す

const $ageInput = $('.js-age_input');
// 値が15以上のinputを抽出する
const thresholdValue = 15;
filterContentValue($ageInput, thresholdValue);

上記のように引数でそれぞれを渡せば

  • 関数実行箇所でDOM取得しているため、参照しているDOMが分かりやすい(変数でgrepすることもできる)
  • 読みやすい(文脈から、15という値を基準に抽出を行うことが分かる)
  • 別の基準値やDOMを渡して関数の再利用ができる

将来基準値やセレクタを変えることがあっても、関数ではなく実行箇所を修正すればよいため、テスト工数が減ります。

★5 哲学

どこまでを1つの関数にまとめるか?

シンプルだけど一番難しいです。
あまりまとめすぎても、

  • 責任が多くなりすぎて再利用が難しくなったり…
  • 引数が膨大になってしまい、かえって読みづらくなったり…

逆に細かくしすぎても冗長なコードになってしまうため、永遠に悩まされるポイントですね。

頂いた指摘の中では「凝集度」という指標がありました。
関数の凝集度が高いと読みやすく、かつ拡張性が高くなります。
目安としては1メソッド1動詞、くらいの気持ちで作るといい感じに区切れる気がします。

例えばこちらの関数

const toggleAccordion = function ($target, isOpen) {
  if (isOpen) {
    // 引数の状態に対して閉じたり開いたりする
    ...
  }
});

toggleAccordionという関数は、「開いた」状態と「閉じた」状態の切り替えをしているため、openAccordioncloseAccordionに分解が可能です。
なので…

const openAccordion = function ($target) {
  // 開ける
});

const closeAccordion = function ($target) {
  // 閉じる
});

とし、関数外でisOpenの値に応じてopenAccordioncloseAccordionを呼んであげる…という書き方ができます。
こうして関数を小さく分けておくと、「やっぱりアコーディオンを開く前に色々したい…」となったときに、新しく機能を追加しやすいなど、拡張性が高くなります。

VSCodeの拡張機能にCodeMetricsというものがあり、こちらは循環的複雑度(に近いもの)を計算して表示してくれるので、関数が大きすぎないか?の目安として使えます。

レビュー観点としてすごく参考になりました→メンバーに恨まれそうな3つのコードレビュー施策を徹底したら、逆にメンバーが爆速で成長した話

変数名・関数名を分かりやすく名付ける

  • 関数名の名付けに悩む場合は、関数が大きすぎる可能性がある(分解すれば名付けやすくなるかも?)
  • filteredListとするより、filteredCheckedListにする。他人が見たときに「具体的に何の値なのか?」が推測しやすい名前をつける
  • getやsetは曖昧すぎるので極力名付けに使わない

などなど…。
毎日「最良の名前とは?」を考えながらコードを書いています。
(レビューで「なるほどな〜〜!」と気付かされることも多いですが!)

これについては記事がまるまる1本書けそうなトピックなので、そのうち自戒として記事に起こしたいですね…。

雑感

悲しくなることも多いのですが、「これは美しいだろ…!」と思って書いたコードが褒められると「この仕事最高〜〜!」と思えるので、これからも読みやすく美しいコードを求めて頑張ります!

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

Flash Advent Calendar 10日目 - WebGL導入への道のり03 -

imageを任意のスケールと座標に描画する事はできたので
lineToやquadraticCurveToといったパスから描画できないかチャレンジしていきます。

らくさんのこの記事を参考にしながら進めていきます。
WebGLでベジェ曲線を描いてみた
続:WebGLでベジェ曲線を描いてみた

上記の記事の実装するにはBufferObjectの使い方を知っておかないと
実装は難しいので簡易版を書こうと思います。

※注意※
らくさんの記事通りに実装すると描画速度は凄く早いのですが
今回の簡易版は導入は比較的簡単ですが、速度は落ちてしまうのでご了承を:bow:

目次

  • 描画用のシェーダーを作る
  • beginPath関数を作る
  • moveTo関数を作る
  • lineTo関数を作る
  • quadraticCurveTo関数を作る
  • fill関数を作る

描画用のシェーダーを作る

頂点シェーダー

setTransform(matrix)で変形させたいので、そこを考慮しながらシェーダーを組んでみます。

const VERTEX_SHADER = "\
precision highp float;\n\
attribute vec2 a_vertex;\n\
uniform vec2 u_viewport;\n\
uniform mat3 u_transform;\n\
void main() {\n\
    vec3 pos = u_transform * vec3(a_vertex, 1.0);\n\
    pos.x = (2.0 * pos.x / u_viewport.x) - 1.0;\n\
    pos.y = -((2.0 * pos.y / u_viewport.y) - 1.0);\n\
    gl_Position = vec4(pos.xy, 0.0, 1.0);\n\
}";

フラグメントシェーダー

今回は色情報だけなので簡易的に組んでいきます。

const FRAGMENT_SHADER = "\
precision mediump float;\n\
uniform vec4 u_color;\n\
void main() {\n\
    gl_FragColor = u_color;\n\
}";

こんな感じで、任意の座標に描画できるようになるかと思います。

では、Canvas2Dで使ってる関数を実装していきます。

beginPath関数を作る

/**
 * @return {void}
 * @public
 */
beginPath ()
{
    this.vertices    = []; // moveToする度にcurrentPathにあるパス情報をまとめる
    this.currentPath = []; // moveToから始まる描画情報
}

moveTo関数を作る

/**
 * @param  {number} x
 * @param  {number} y
 * @return {void}
 * @public
 */
moveTo (x, y)
{
    // 直線のパス情報を一回まとめる
    if (this.currentPath.length) {
        this.vertices.push(this.currentPath.slice(0));
    }

    // 初期化
    this.currentPath = [];

    // 開始
    this.currentPath.push(x);
    this.currentPath.push(y);
};

lineTo関数を作る

/**
 * @param  {number} x
 * @param  {number} y
 * @return {void}
 * @public
 */
lineTo (x, y)
{
    // 開始情報がない時は(0,0)から出発させる
    if (this.currentPath.length === 0) {
        this.moveTo(0, 0);
    }

    this.currentPath.push(x, y);
};

quadraticCurveTo関数を作る

/**
 * @param  {number} cx
 * @param  {number} cy
 * @param  {number} dx
 * @param  {number} dy
 * @return {void}
 * @public
 */
quadraticCurveTo (cx, cy, dx, dy)
{
    // 開始情報がない時は(0,0)から出発させる
    if (this.currentPath.length === 0) {
        this.moveTo(0, 0);
    }

    // 直前のx,y座標をセット
    const fromX = this.currentPath[this.currentPath.length - 2];
    const fromY = this.currentPath[this.currentPath.length - 1];

    let xa = 0;
    let ya = 0;

    // TODO 本来はちゃんと計算した方いいけど
    // swfでは20もあれば十分滑らかな曲線を描けるので固定
    const length = 20;
    for (let idx = 0; idx < 20; idx++) {
        const f = idx / length;

        // 開始座標から目的座標までの外周のパスをpushしていく
        xa = fromX + ((cx - fromX) * f);
        ya = fromY + ((cy - fromY) * f);
        this.currentPath.push(xa + (((cx + ((x - cx) * f)) - xa) * f));
        this.currentPath.push(ya + (((cy + ((y - cy) * f)) - ya) * f));
    }
}

fill関数を作る

事前にattributeの部分を汎用的に使えるようにしておきます。

attributeの準備

/**
 * @return {void}
 * @public
 */
createBuffer = function ()
{
    // reset
    this.attributes = [
        {
            "name": "a_vertex",
            "value": {
                "streamType": gl.STREAM_DRAW,
                "spacing": 2,
                "buffer": null,
                "value": [0,0, 0,0, 0,0],
                "location": 0
            }
        },
        // ... 以下割愛
    ];

    // set attributes
    var length = this.attributes.length;
    for (let idx = 0; idx < length; idx++) {

        var attribute = this.attributes[idx];

        var buffer = this.gl.createBuffer();
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
        this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(attribute.value.value), attribute.value.streamType);

        attribute.value.location = this.gl.getAttribLocation(this.program, attribute.name);
        this.gl.enableVertexAttribArray(attribute.value.location);

        // clear
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);

        // set
        attribute.value.buffer = buffer;
    }
}

情報を受け取る関数も準備しておく

updateBuffer () 
{
    const length = this.attributes.length|0;
    for (let idx = 0; idx < length; idx++) {

        const attribute = this.attributes[idx];

        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, attribute.value.buffer);


        this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(attribute.value.value), attribute.value.streamType);

        this.gl.vertexAttribPointer(attribute.value.location, attribute.value.spacing, gl.FLOAT, false, 0, 0);

        // clear
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);

    }
}

fill関数

/**
 * @return {void}
 * @public
 */
fill ()
{
    // 最低限の三角形がない場合は何もしない
    if (this.currentPath.length < 6) {
        return ;
    }

    // set
    this.vertices.push(this.currentPath.slcie(0));

    // clear
    this.currentPath = [];

    this.gl.useProgram(this.program);

    // Uniformの処理は9日目の記事を参照ください。
    this.updateUniforms([
        { "name": "u_color", "value": this.fillStyle },
        { "name": "u_transform", "value": this.matrix },
        { "u_viewport": [this.canvas.width, this.canvas.height] } // <= ここは用途に合わせて可変
    ]);

    const length = this.vertices.length;
    for (let idx = 0; idx < length; idx++) {

        var vertex = this.vertices[idx];
        this.attributes["a_vertex"].value = vertex;
        this.updateBuffer();
        this.gl.drawArrays(this.gl.TRIANGLE_FAN, 0, vertex.length / 2);
    }

    // 初期化
    this.vertices = [];
}

最終的には上記の記事にあるフラグメントシェーダーの補完機能を利用した手法の描画に変更するのですが
まず始める!っというところとしては、こんな感じで開始しました。

個人的にはこの辺りからWebGLって凄く楽しい!!
ってなってきました。

今ではWebGL大好きってなってますw

次は、WebGLによくある、あれ?WebGLなのに遅くない??
って問題が多発したので、その時の事を書こうと思います。

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

はじめまして

自己紹介

はじめまして。情報系大学3年のTaYです。普段はPHPやJSを触っています。
最近ではゼミでPythonも触っています。(ほんとに触っているだけ)

自分のやってきたことの備忘録として始めてみました。

今はLaravelを独学中で簡単な掲示板を作っています。

完成したら載せようかな〜と考えています!

特に書くこともないので今回はこの辺で:wave:

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

(我が家のPC運用)ダッシュボードで運用情報を確認

我が家の運用情報の確認

前回前々回とWindowsバッチによるバックアップ、イベントログ収集を実行する記事を書きました。
上記のコードは「こちら」に格納していますが、左記にあるバッチは実行結果をレポートファイルに出力するようにしています。
今回、これらのファイルを読み込んで画面に出力するPC運用情報表示アプリケーションをElectronで作成したので記事にしたいと思います。

画面について

画面はこのようになっています。
PC保守情報1.png

  • 1つの枠で、1つの運用バッチの実行結果を示します。
  • 成功や異常なしは青、異常がある場合は赤で表示します。
  • 詳細情報がある場合は枠内右に表示します。

枠をクリックで詳細情報の非表示/表示が切り替わります。すべての詳細を非表示にするとこのようになります。
PC保守情報2.png

アプリケーションの動きについて

PC運用情報表示アプリケーションは、各種運用バッチが出力するレポートファイルを読み込んで表示します。
イメージ図です。難しい処理はしていません。
PC保守情報3.png
複数のレポートファイルは一か所のフォルダ内に格納し決まった形式にする必要があります。

  • 「[HEAD]日時,結果」で実行結果を
  • 「[REPORT]日時,詳細」で詳細を複数行

例として、こんな感じです。

[REPORT][NOTICE!]2020/12/11 23:11:44,警告,(1006 マルウェア対策エンジンが、マルウェアまたは他の望ましくない可能性のあるソフトウェアを検出しました。)
[REPORT][NOTICE!]2020/12/13 00:39:50,警告,(1006 マルウェア対策エンジンが、マルウェアまたは他の望ましくない可能性のあるソフトウェアを検出しました。)
[REPORT]2020/12/11 22:04:30,情報,(1001 マルウェア対策スキャンが終了しました)
[REPORT]2020/12/10 03:05:36,情報,(2002 マルウェア対策のエンジンが正常に更新されました。)
[REPORT]2020/12/13 04:05:41,情報,(2000 マルウェア対策の定義が正常に更新されました。)
[HEAD]2020/12/13 23:50:06,異常発生

こちら」に公開しているコードを使用する場合、環境に合わせて以下の箇所をカスタマイズします。

  • main.js 以下の箇所でレポートファイルのパスを定義しています。
    // ここにレポートファイルのパスを定義する
    event.returnValue = app.getAppPath() + "\\sampleReport";
  • index.html レポートファイルを表示する属性です。
    <div class="area_report" id="area_report_bkup"></div>
    <div class="area_report" id="area_report_bwch"></div>
  • commonPcmaint.js レポートファイルを読み込む処理です。
    // 通知領域を読み込む
    readNotice() {
        this._parseAndDisplayReport("area_report_bkup", "report_bkup.txt", "バックアップ(データ)");
        this._parseAndDisplayReport("area_report_bwch", "report_bwch.txt", "ブラウザキャッシュ");
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む