20200118のJavaScriptに関する記事は19件です。

[Github Actions]githubのリリースの公開に合わせてtwitterで通知

前回の記事(webサイトの更新の表示をgithubのreleaseで管理)の中で書いたgithubのreleaseに合わせてツイートをするGithub Actionsについて書きます。

概要

githubの任意のリポジトリのreleaseが公開されたタイミングでツイートをするGithub Actionsを作っていきます。

GitHub Marketplace
https://github.com/marketplace/actions/tweet-trigger-publish-release

リポジトリ
https://github.com/mugi111/tweet-trigger-release

作り方

↓↓↓作り方の参考にしたページです↓↓↓
LGTMすると現場猫が「ヨシ!」してくれるGitHub Actionsをつくった + Tips

今回はこちらのjavascriptのテンプレートを使用して作っていきました。

githubにログインをしてリポジトリのページに行くと、緑色のUse this templateというボタンが見えるので
image.png

いつも通りの見慣れたページに移動するので、ここから自分のリポジトリを作成していきます。
image.png

テンプレートに加えて使うものとしてものとして今回はtwitterのパッケージをインストールします

npm
npm i twitter --save
yarn
yarn add twitter

作りは単純でtwitterのAPIを使用してツイートをPOSTするだけです。
clientの宣言やツイート本文の指定で使われている

core.getInput(' xxx ')

この記述で後述する.github/workflow直下のyamlファイルから値を取得してきます。

下がコードの全文です。

index.js
const core = require('@actions/core');
const Twitter = require('twitter');

// most @actions toolkit packages have async methods
async function run() {
  try {
    const client = new Twitter({
      consumer_key: core.getInput('consumer_key'),
      consumer_secret: core.getInput('consumer_secret'),
      access_token_key: core.getInput('access_token_key'),
      access_token_secret: core.getInput('access_token_secret')
    });

    client.post('statuses/update', {status: core.getInput('tweet_body')}, (error) => {
      if (!error) {
          console.log('Succeeded!');
      } else {
          console.log('Couldnt tweet.');
      }
  });
  } 
  catch (error) {
    core.setFailed(error.message);
  }
}

run()

yamlファイルの中身

この部分でreleasepublishedをトリガーにすることを定義しています。

on:
  release:
    types: [published]

あとは実行環境や使用するActions、入力する値を設定します。

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: mugi111/tweet-trigger-release@v1.2
        with:
          consumer_key: ${{ secrets.CONSUMER_KEY }}
          consumer_secret: ${{ secrets.CONSUMER_SECRET }}
          access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }}
          access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }}
          tweet_body: "更新しました!"

使い方

以下のyamlファイルを.github/workflowの直下においておきます。

tweet-trigger-release.yml
name: "tweet-trigger-release"
on:
  release:
    types: [published]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: mugi111/tweet-trigger-release@v1.2
        with:
          consumer_key: ${{ secrets.CONSUMER_KEY }}
          consumer_secret: ${{ secrets.CONSUMER_SECRET }}
          access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }}
          access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }}
          tweet_body: "更新しました!"

ここのsecrets.XXXを設定するためにSettingsのSecretsタブからtwitterのDeveloperページから取得したtokenやkeyを登録します。
image.png

Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ ※2019年8月時点の情報
https://qiita.com/kngsym2018/items/2524d21455aac111cdee

あとは"更新しました!"となっているtweet_bodyを書き換えることでツイートの本文を設定して完了です。

おわり

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

便利ページ:データURLを生成する

便利ページ:Javascriptでちょっとした便利な機能を作ってみた」のシリーズものです。

HTMLでは、スキームが先頭についた URL で、コンテンツ制作者は小さなファイルをインラインで文書に埋め込むことができる「データURL」というのがよく使われています。

こんな感じです。

data:[<mediatype>][;base64],<data>

(参考) データURL : MDN
https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/Data_URIs

(たいしたことはやってませんが)便利ページ に追加しておきました。

いつもの通りGitHubにも上げてあります。
 https://github.com/poruruba/utilities

参考までに、以下にデモとしてアクセスできるようにしてあります。「バイナリファイル」のタブを選択してみてください。
 https://poruruba.github.io/utilities/

start.js
        binary_open: function(e){
            var file = e.target.files[0];
            var reader = new FileReader();
            reader.onload = (theFile) =>{
                this.binary_data = new Uint8Array(theFile.target.result);
                this.binary_type = file.type ? file.type: 'application/octet-stream';
                if( this.binary_type.startsWith('text/'))
                    this.binary_text = decoder.decode(this.binary_data);
                this.binary_dataurl = "data:" + this.binary_type + ";base64," + bufferToBase64(this.binary_data);

                this.binary_change();
            };
            reader.readAsArrayBuffer(file);
//            reader.readAsBinaryString(file);
//            reader.readAsDataURL(file);
        },

以下、抜粋です。

this.binary_dataurl = "data:" + this.binary_type + ";base64," + bufferToBase64(this.binary_data);

以上

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

JavaScript参考サイト(個人的メモ)

JavaScript初心者向け参考記事

2017年から始めるjavascript勉強ノート
ES2015(ES6) 入門
Google流JavaScriptにおけるクラス定義の実現方法(ES6以前)
※引用{ES6でクラスが導入されほとんどのブラウザがサポートしている2019年現在、ここで書かれている手法を直接使用することはないでしょう。 }

[JavaScript] 猿でもわかるクロージャ超入門 まとめ

直ぐに忘れるので備忘録
 即時関数 (function(){..})();

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

【Angular】画面真っ白。Failed to load module script: The server responded with a non-JavaScript MIME type of "text/html" がコンソールに表示

1. エラー詳細

Angularアプリをherokuでデプロイして、Build succeeded!されたけど
実際開いてみたら画面真っ白。
そして、コンソールにエラーメッセージが表示されていました。

Failed to load module script: The server responded with a non-JavaScript MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec.

モジュール読み込み失敗。サーバーはJavaScriptじゃない"text/html"のMIME型を返却。

2. 環境

macOS Mojave 10.14.6

Angular CLI: 8.3.22
Node: 12.10.0
Angular: 8.2.14
tsconfig.json

  "compilerOptions": {
    //JavaScirptのバージョン
    "target": "es2015",
  },

3. 解決策

index.htmlにあったscriptタグのtype属性をtext/javascriptに変更

index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>sample-app</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.f88876c1aa2c513e53bf.css"></head>
<body>
  <app-root></app-root>


 <!-- scriptタグのtype="module"をすべて type="text/javascript"に変更 -->

<script src="runtime-es2015.edb2fcf2778e7bf1d426.js" type="text/javascript">
</script>
<script src="runtime-es5.edb2fcf2778e7bf1d426.js" nomodule defer></script>
<script src="polyfills-es5.6696c533341b95a3d617.js" nomodule defer></script>
<script src="polyfills-es2015.2987770fde9daa1d8a2e.js" type="text/javascript"></script>
<script src="main-es2015.bb16c3796b9468f94a5b.js" type="text/javascript"></script>
<script src="main-es5.bb16c3796b9468f94a5b.js" nomodule defer></script></body>
</html>

変更後、再度デプロイしてみたところCSSも含めてすべて表示されるようになりました。

4. 参考サイト

MIMEタイプってなんだ?

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

graphql clientはApolloよりfetchでやった方がいいのではないかという件

リポジトリ

https://github.com/gqlkit-repos/gqlkit-store
graphql client storeプラグインです。
これを、各フレームワークのcontextに突っ込んでやろうという魂胆
reduxはreact、vuexはvueという感じですが
クロスフレームワークでかつgraphqlの為の
client&storeというのを作ってみました。
nuxt.jsでは検証済

resolverファーストアーキテクチャのgraphql client設計

.
├── index.js
└── resolvers
    ├── Mutation
    │   ├── createStaff.js
    │   └── deleteStaff.js
    ├── Query
    │   └── staffs.js
    ├── cache.js
    └── client.js

たったのこれだけ
と言いたいところですが
これを例えば、nuxtで使うなら、nuxtのpluginsディレクトリに突っ込むので
一見、まぁまぁな規模感あるように見えます。

というか、そもそもなのですが
クライアント側のvuexとかreduxだとかのストアが抱えている課題って
もうストアはストアで別個のフレームワークと捉えて設計した方が良いレベルではないかと思うので
これはなんか正解路線なのでは?と思っています。

ちなみにQueryとMutation以下のjsファイルはデモ用のファイルです。

queryのコードの見た目も、mutationのコードの見た目もほぼ同じ

上のディレクトリツリーのstaffs.jsはこんな感じ

staffs.js
import client from '../client'
import cache from '../cache'
import gql from 'graphql-tag'

export const demand = gql`
    query {
        staffs {
            id
            name
            age
            sex
        }
    }
`

export default async variables => {
    let re
    if (!cache.has('staffs').value()) {
        const { staffs } = await client.req(demand)
        cache.set('staffs', staffs).write()
        re = staffs
    } else {
        re = cache.get('staffs').value()
    }

    return re
}

mutationにあたるcreateStaff.jsはこんな感じ
mutationはrefetchしたいモジュールファイルをrefetchという名前で読み込みます。
そんでもってqueryでもmutationでも
クライアントツールはclient.req(demand, variables)(variablesが必要ない場合は省略)

createStaff.js
import client from '../client'
import cache from '../cache'
import gql from 'graphql-tag'
import refetch from "../Query/staffs";

export const demand = gql`
    mutation($name: String!, $age: Int!, $sex: String!) {
        createStaff(name: $name, age: $age, sex: $sex) {
            id
            name
            age
            sex
        }
    }
`

export default async variables => {
    const res = await client.req(demand, variables)
    const staff = res.createStaff
    const staffs = cache.get('staffs').value()
    staffs.push(staff)
    cache.set('staffs', staffs).write()

    return refetch()
}

クライアントツールは21行のコードでqueryもmutationもOK!

fetchで書くとこんなシンプルになるとは正直、驚きました。
ただ、今後subscription導入のことも考えると
もうちょっと大きいファイルになる可能性はあります。
まだ、subscriptionは未経験なので。。。

client.js
const GQLKIT_SERVER_END_POINT = process.env.GQLKIT_SERVER_END_POINT || 'http://localhost:4000/query'
const method = 'POST'
const headers = {
    'Content-Type': 'application/json',
    Accept: 'application/json'
}

export default {
    async req(demand, variables) {
        const res = await fetch(GQLKIT_SERVER_END_POINT, {
            method,
            headers,
            body: JSON.stringify({
                query: demand.loc.source.body,
                variables
            })
        })
        const { data } = await res.json()
        return data
    }
}


キャッシュはどうするか?

キャッシュは完全切り分けの方針です。
Apolloのapiはキャッシュと密結合ですが
resolverファーストなgraphql clientアーキテクチャでは
キャッシュは完全別扱いにして
好きなin memory dbを使えば良いという方針で考えます。
resolverを書いて解決すれば良いじゃないか!という方針です。

僕は今のところlowdbを使っています。
ここはお好みで好きなもの使ってよしです。

cache.js
import low from 'lowdb'
import Memory from 'lowdb/adapters/Memory'

const cache = low(new Memory())

cache.defaults({}).write()

export default cache

まとめ

apolloがなぜ、あれほどまでに複雑なアーキテクチャを取っているのか
僕の知らない何らかの理由でああなっている可能性も大なので

これがベストアンサーだなんて到底言い切る事はできないです。
これはあくまで、1アイデアというものです。

あと、jsのオブジェクトをgormっぽい書き方で読み書き出来るような
ツールをご存知の方おられましたら教えて頂けると嬉しいです。

サーバーとクライアントで違うノウハウというのを最小限に抑えたい
ということで
graphqlサーバーのアーキテクチャを真似たプラグインを作ってみたという話でした。

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

Firebase 静的サイト(html CSS JavaScript)をデプロイ

Firebaseにてデプロイの記録

【使用環境】
html
CSS
JavaScript
macbook

<次回の時短の為に記録>


Firebaseにて登録、 (googleアカウント必要)ログイン

https://firebase.google.com/

新しいプロジェクトフォルダをFirebase HP内で作成する。
スクリーンショット 2020-01-18 17.31.20.png


ここからターミナルで

今回は下記②の設定のために public ディレクトリ 配下にデプロイアプリ(html CSS JavaScript img等)を配置

URL > ルートパスの為


デプロイにGitHubは必須ではないが、
git add ←必要なら
git commit   ←必要なら
push ←必要なら


① firebase-tools をインストール

(npm がインストールされている前提)
$ npm install -g firebase-tools

$ npm -v

$ firebase -V

$ firebase login

アプリディレクトリへ移動後、

①初期化

$ firebase init

この画面が出る
スクリーンショット 2020-01-18 16.53.21.png

PCの矢印キーで Hosting:  を選択。 (色が変わる。かつ●へ)
エンター
スクリーンショット 2020-01-18 16.53.46.png

firebaseのHPで作成した

アプリを関連付けるプロジェクトを選択するよう求められる

Use an existing project を選んで エンター
スクリーンショット 2020-01-18 16.55.55.png

HPで作成したプロジェクト一覧がでる。
今回のものを選択し、エンター

(画像はありません)

ディレクトリのパスを聞かれるのでデフォルト (/public) のまま エンター

スクリーンショット 2020-01-18 17.19.42.png

Configure as a single-page app (rewrite all urls to /index.html)? (y/N)
他も動かすため、推奨 Nを選択

File public/index.html already exists. Overwrite?
作成したindex.htmlが上書きされるので 推奨 Nを選択

complete!

スクリーンショット 2020-01-18 17.23.30.png

初期化が完了


デプロイ

そのままの階層で 

$ firebase deploy

スクリーンショット 2020-01-18 17.25.57.png

↑ ここの下に URLがある。

デプロイ完了

(修正後の再デプロイは、$ firebase deployだけで↑は不要)


2回目以降の事象

$ firebase login

>Error: Server Error. certificate has expired

スクリーンショット 2020-01-18 16.47.15.png

その為、

firebase-toolsをupdateする必要がある

$ sudo npm i -g firebase-tools 

$ firebase login   

ログインできた

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

DOMにおける「Illegal Invocation」エラーの一例とその原因・対処法

DOMに関わるプログラムを書いていると、「Illegal Invocation」エラーに遭遇することがあります。例えば、次のようにするとqの呼び出しでエラーが発生します。

const q = document.querySelector;
q("body"); // Uncaught TypeError: Illegal invocation

ちなみに、「Illegal Invocation」とはGoogle Chromeのエラーメッセージであり、Firefoxは次のようにもうちょっと親切なエラーメッセージを出力します。エラーメッセージというのは基本的に仕様で定められておらず処理系の裁量があるところですから、エラーの意味が分からなくて詰まったら別のブラウザで試してみるのもひとつの手です。

TypeError: 'querySelector' called on an object that does not implement interface Document.

Google Chromeの「Illegal Invocation」はいろいろな場合に表示されるメッセージなので、全てに通用する汎用的な原因・対処法というのはありません。この記事で扱うのは冒頭のように、「呼び出しやすいようにメソッドを別の変数に入れた」場合に発生するエラーです。

Twitterでこのエラーで詰まっている方がいたのですが、「Illegal Invocation」で検索しても出てくる既存の記事がどれも微妙だったのでこの記事を用意しました。

エラーの原因

このエラーの原因をひとことで言うならば「thisが違う」ということです。DOMの関数は、正しいthisで呼び出さないといけません。

JavaScriptは、オブジェクトのメソッド内ではthisを用いて自分自身を参照することができます。

const obj = {
  prop: 100,
  method() {
    console.log(this.prop);
  }
};
obj.method(); // 100 が表示される

この例では、obj.method()という呼び出しをすると100が表示されます。これは、obj.methodの中で使われているthisobjを指すからです。しかし、このプログラムを次のように変えるとうまく動作しません。

const obj = {
  prop: 100,
  method() {
    console.log(this.prop);
  }
};
const func = obj.method; 
func(); // undefined が表示される(strictモード内ならエラーが発生)

obj.methodを別の変数funcに入れてfuncを呼び出したら、結果が変わってしまいました。これは、obj.methodの呼び出し方が変わったことによりobj.methodの中のthisが変わったからです1

このように、関数内でのthisがどうなっているかは、その関数をどのように呼び出したかによって決まります。foo.bar()というメソッド呼び出しの記法を用いて呼んだ場合は、関数bar内でのthisの値はfooです。つまり、foo.bar()というメソッド呼び出しの記法は、ただ単に「foo.barに入っている関数を呼び出す」という意味なのではなく、「thisfooとして、foo.barを呼び出す」とおいう意味のプログラムだったのです。

冒頭のdocument.querySelectorについても同じことが言えます。このquerySelectorメソッドは、thisdocumentの状態で呼び出さないといけません。よって、「thisdocumentとして呼び出す」というプログラムを書かないと正しく呼び出すことができません。

const q = document.querySelectorとしたあとにq()を呼び出した場合は、qはたしかにdocument.querySelectorと同じであるものの、q()として呼び出してもthisdocumentになりません。これが「Illegal Invocation」エラーの原因でした。

ここで、Firefoxのエラーメッセージを振り返ってみましょう。

TypeError: 'querySelector' called on an object that does not implement interface Document.

日本語にすると「querySelectorDocumentインターフェースを実装していないオブジェクト上で呼ばれました」となります。「~上で」という表現がわかりにくいですが、これはthisが何かということを意味しています。つまり、これは「thisDocumentインターフェースを実装したオブジェクトではない」ということに文句を言っているのです。まさに上で解説した通りのことが起きていることになります。個人的には、エラーメッセージに「this」というワードを含めてくれればもっと親切になるのにと思わないでもありませんが。

エラーの対処法

以上で、エラーの原因は分かりました。次は、これに対する対処法を説明します。基本的には、問題が「thisが正しくない」ことだったのですから、対処法は「正しいthisで呼ぶ」しかありません。その方法は色々あります。

最もベーシックな方法は、常にメソッド呼び出しの記法を使うことです。document.querySelector("body")の形で呼べば、この記法は「thisdocumentとしてメソッドを呼ぶ」という意味ですから正しいthisで呼ぶことができます。

しかし、これでは冒頭のコードの目的を達成したとは言えません。冒頭のコードの目的は、これをq("body")という短いコードで呼べるようにすることでした。これを達成する方法のひとつは、次のようにすることです。

const q = (query) => document.querySelector(query);

q("body"); // エラーが出ない!

これは元のプログラムをη変換しただけのように見えますが、document.querySelectorを呼び出すときはあくまでメソッド呼び出しの記法を使われているのがポイントです。これならquerySelectorを呼び出すときのthisがちゃんとdocumentになっているため、エラーにはなりません。

別の方法はbindを使って次のようにする方法です。

const q = document.querySelector.bind(document);

q("body"); // エラーが出ない!

bindをこのように使う場合、「thisがすでに固定されている関数」を作ることができます。この場合変数qに入っているのは、「thisdocumentに固定されたdocument.querySelector」です。よって、q("body")のようにメソッド記法を使わずに呼び出した場合でも、bindの効果によってdocument.querySelectorthisdocumentの状態で呼び出されるため、エラーにはなりません。

もうひとつ別の方法としては、callメソッド(またはapplyメソッド)を用いる方法もあります。これは関数オブジェクトが持つメソッドで、「thisを明示的に指定して呼び出す」ということが可能になります。

const q = document.querySelector;

q.call(document, "body"); // エラーが出ない

これは長いので、短く書くという目的に使われることは無いと思いますが、callapplythisの値も含めて関数呼び出しを詳細に制御したい場合に重宝します。

上級編:エラーを仕様で確かめる

ところで、「document.querySelectorthisdocumentの状態で呼び出さないとエラーになる」というのは不親切にも思えますが、実はちゃんと仕様書に明記してあり、ブラウザはそれに従っているだけです。

そこで、この仕様がどのように定義してあるのかを確かめましょう。

ということで、まずDOM仕様書をチェックしましょう。querySelectorParentNodeインターフェースの中に定義してあります。仕様にはこのように定義が書かれています。

interface mixin ParentNode {
  // (省略)
  Element? querySelector(DOMString selectors);
  [NewObject] NodeList querySelectorAll(DOMString selectors);
};
Document includes ParentNode;

なんとなく、querySelectorParentNodeインターフェースが持つメソッドであり、引数はDOMString型(JavaScriptでいうただの文字列です)の引数selectorsひとつであり、返り値はElement?型(Elementまたはnullを意味します)であることが分かります。

実は、この記法はWebIDLと呼ばれるものです。WebIDLはインターフェース・メソッド等を定義するための標準化された言語であり、Web関連のさまざまな仕様書におけるインターフェース定義記法を統一するために作成されました。仕様書の書き方を定義するメタ仕様という感じです。

そして、今回目的としている挙動の根拠を見つけるためには、WebIDL仕様書を紐解かなければなりません。つまり、「Element? querySelector(DOMString selectors);と書かれていたらどのような挙動のメソッドになるのか」という定義をWebIDL仕様書で調べることになります。

現在Webを支配しているのはJavaScript (ECMAScript) ですから、WebIDLにはECMAScript bindingという章があり、「WebIDLで定義されたメソッドはJavaScriptではどういう挙動をするのか」ということが厳密に仕様化されています。

いきなり核心に迫りますが、3.6.7 Operationsという節でこの構文に対する仕様が定義されています。最初の段落を引用します(強調は筆者)。

For each unique identifier of an exposed operation defined on the interface, there exist a corresponding property. Static operations are exposed of the interface object. Regular operations are exposed on the interface prototype object, unless the operation is unforgeable or the interface was declared with the [Global] extended attribute, in which case they are exposed on every object that implements the interface.

強調されている点が今回関係のある部分です。上記の記法によれば、実はquerySelectorはregular operationに分類されます。よって、querySelectorはinterface prototype objectに実体が存在することになります。今回querySelectorParentNodeというmixinインターフェース上に定義されていますが、Document includes ParentNode;という宣言がありますから、querySelectorDocumentインターフェース上に宣言されたものであると見なされます。よって、ここで言うinterface prototype objectとはDocument.prototypeのことです。

// true が表示される
console.log(document.querySelector === Document.prototype.querySelector);

となると、Document.prototype.querySelectorにどのような関数が設定されるのかがポイントになります。これは、仕様書を少し読み進めると出てくるcreate an operation functionアルゴリズムで定義されています。このアルゴリズムは「関数を作る」という複雑な操作を自然言語で表現しているためややこしいのですが、一部を抜粋すると、Document.prototype.querySelectorを実行した際にはまず以下のステップが実行されます。

If target is an interface, and op is not a static operation:

  1. Let esValue be the this value, if it is not null or undefined, or realm’s global object otherwise. (This will subsequently cause a TypeError in a few steps, if the global object does not implement target and [LenientThis] is not specified.)
  2. If esValue is a platform object, then perform a security check, passing esValue, id, and "method".
  3. If esValue does not implement the interface target, throw a TypeError.
  4. Set idlObject to the IDL interface type value that represents a reference to esValue.

ここではtargetopという2つの変数が存在しており、それぞれinterfaceとoperationです。これらはJavaScriptの値ではなく、WebIDLにおけるインターフェース・オペレーションといった概念です。言うなればこれらはWebIDLのASTノードのことであると見なせます。ここではtargetquerySelectorの実装先であるDocumentインターフェースを表すものであり、opは`Element? querySelector(DOMString selectors);という定義そのものであると考えられます。

実際にこのステップを実行すると、まず1により、thisの値が取得されesValueに入ります。document.querySelector("body")のように呼び出した場合はesValueに入るのはdocumentであり、正しくない方法で呼び出した場合はesValueにグローバルオブジェクトが入ります。

2にはsecurity checkという文言が登場しますが、これは今回は関係ありません。ちなみに、security checkの実態はHTML仕様書にあり、これは他のオリジンのブラウジングコンテキストに対して許可されていない操作をしたらSecurityErrorが発生するという仕様を定めています。

3が一番のポイントです。ここでは、esValuetargetをimplementしないならば、TypeErrorを発生させると定義されています。esValuethisの値で、targetはDocumentインターフェースのことでしたから、ここで問題のチェックが行われていることになります。ちゃんとthisの値をdocumentにして呼び出さなかった場合、esValueはDocumentインターフェースをimplementしていませんから、TypeErrorが発生することになります。

なお、ここで「implementする」というのがどういう意味なのかが気になるかと思います。これは3.7 Platform objects implementing interfacesで以下のように定義されています。

An ECMAScript value value implements an interface interface if value is a platform object and the inclusive inherited interfaces of value.[[PrimaryInterface]] contains interface.

つまり、value.[[PrimaryInterface]]にそのインターフェース(もしくはそれを継承したインターフェース)が入っているかどうかでチェックしています。[[PrimaryInterface]]というのはインターナルスロットです。これも仕様書用語ですが、インターナルスロットについては筆者が最近書いた別の記事で詳しく解説しています。これはWebIDLによって新たに定義されているインターナルスロットです。

なお、このような方式になっているということは、オブジェクトのプロトタイプをごまかしても無意味だということを意味しています。次のような「偽Doocument」を作ってもごまかすことはできずにTypeErrorが発生します。

const fakeDocument = Object.create(Document.prototype);
fakeDocument.querySelector = document.querySelector;

fakeDocument.querySelector("body"); // TypeErrorが発生

まとめ

この記事では、DOMのメソッドを正しいthisで呼び出さなかったことに起因する「Illegal Invocation」エラーを解説し、その対処法もあわせて説明しました。ポイントは、メソッド呼び出しのfoo.bar()という記法はただ関数を呼び出すだけでなく、「呼び出された関数の中のthisを指定する」という機能も併せ持っているという点です。DOMに限らず、多くのメソッドは正しくthisを指定しないと期待した通りに動作しません2。これにより、「メソッドを別の変数に入れてそれを呼び出す」ということをした場合はメソッド呼び出し記法を使っていないため正しいthisがセットされず、エラーとなるのでした。

対処法としては、第一に「ちゃんとメソッド呼び出し記法を使って呼び出す」が上げられます。また、Function.prototype.bindを使ってthisが常に固定された関数を作る方法もありました。冗長ですが、Function.prototype.callthisを明示的に指定する方法もあります。

この記事の後半では上級者向けのコンテンツとして、「thisが正しくないとTypeErrorが発生する」という挙動がちゃんと仕様書において定義されていることを確かめました。


  1. この呼び出し方の場合、strictモードではthisundefinedになり、それ以外の場合はthisはグローバルオブジェクト(globalThis)になります。 

  2. 最近はclass宣言の中でプロパティ宣言とアロー関数を使ってメソッドを宣言する流派もあり、その場合はアロー関数によってthisが固定されるためどのように呼び出しても期待通りのthisとなります。 

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

Javascript + CSS で複数行テキストを省略する

実現したいこと

jQueryを使わずに複数行テキスト末尾に「...」をつけたい。
よくある処理なのに忘れがちなので備忘録。

TL;DR

https://playcode.io/484998
スクリーンショット 2020-01-18 15.22.24.png

参考

クラスメソッドさんの記事を土台にVanilla JSに書き換えました。
クラスメソッドさん、いつもありがとうございます。
https://dev.classmethod.jp/ria/string-replace-css-and-jquery/

コード

index.html
<div class="wrap">
  <p class="ellipsis">
    あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら、
    うつくしい森で飾られたモリーオ市、
    郊外のぎらぎらひかる草の波。
  </p>
</div>
ellipsis.css
.ellipsis {
  width: 296px;
  height: 48px;
  font-size: 16px;
  overflow: hidden;
}
ellipsis.js
const target = document.getElementsByClassName("ellipsis")[0];
let text = target.innerHTML;

const clone = target.cloneNode(true);
clone.style.visibility  = "hidden";
clone.style.position    = "absolute";
clone.style.overflow    = "visible";
clone.style.width       = `${target.clientWidth}px`;
clone.style.height      = "auto";

target.insertAdjacentElement("afterend", clone);

while((text.length > 0) && (clone.clientHeight > target.clientHeight)) {
  text = text.substr(0, text.length - 1);
  clone.innerHTML = `${text}...`;
}

target.innerHTML = clone.innerHTML;
clone.parentNode.removeChild(clone);

解説

▶︎何もしていない状態

ellipsis.css
.ellipsis {
  width: 296px;
  font-size: 16px;
}

スクリーンショット 2020-01-18 15.34.42.png

▶︎高さをheight: 48px;で指定し、要素からはみ出したテキストをoverflow: hidden;で隠す

ellipsis.css
.ellipsis {
  width: 296px;
  height: 48px;
  font-size: 16px;
  overflow: hidden;
}

スクリーンショット 2020-01-18 15.51.32.png

▶︎JSの処理を追加
テキストの<p>要素を複製して、直後に追加

ellipsis.js
const target = document.getElementsByClassName("ellipsis")[0];
let text = target.innerHTML;

const clone = target.cloneNode(true);
clone.style.visibility  = "hidden"; // display: "none"だとclientHeightが取得できない
clone.style.position    = "absolute";
clone.style.overflow    = "visible";
clone.style.width       = `${target.clientWidth}px`;
clone.style.height      = "auto";

target.insertAdjacentElement("afterend", clone);

▶︎複製した要素を追加した状態
スクリーンショット 2020-01-18 15.54.48.png

▶︎複製した要素のheightが元の要素のheightを下回るまで文字を減らしていく

ellipsis.js
while((text.length > 0) && (clone.clientHeight > target.clientHeight)) {
  text = text.substr(0, text.length - 1);
  clone.innerHTML = `${text}...`;
}

▶︎複製した要素のテキストを元の要素に代入、複製要素は不要なので削除

ellipsis.js
target.innerHTML = clone.innerHTML;
clone.parentNode.removeChild(clone);

:100::v::100::v::100::v::100::v::100:
スクリーンショット 2020-01-18 15.22.24.png

複製要素もちゃんと削除されてます:ok_hand:
スクリーンショット 2020-01-18 16.13.47.png

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

初心者による jQuery 基礎

jQueryの基本的な構文

jQueryで行われる処理の流れは、

1. $()関数で命令の対象となるHTML要素をjQueryオブジェクトに変換
2. そのjQueryオブジェクトに対して独自メソッドを呼び出して変更を加える

jQueryの基本的な構文は、

sample.js
//jQueryオブジェクト.変更する命令
$(HTML要素).メソッド('引数', '引数')

また、HTMLを最後まで読み込んだ時点で実行するために、jQueryは以下のようになる。

sample.js
$(function){
 //HTMLがロードされたのちに実行する処理
}

onメソッド

onメソッド構文は次のように書く

sample.js
on('イベントタイプ', 'イベントハンドラ')

ちなみに、thisプロパティは「onメソッドに指定してあるイベントが発生した要素」が格納される。
thisを使うことで、

1. 処理のパフォーマンスが上がる
2. コードの汎用性を持たせられる
そして一番重要なのが、
3. $()関数に複数のセレクタを指定した場合にthisを使うことで処理の切り分けをしてくれる

というメリットがある。

イベントタイプ

主なイベントタイプは、

mouseover : 要素にマウスが乗った時
mouseout : 要素からマウスが離れた時
mousedown : 要素上でクリックボタンが押されたとき
mouseup : 要素上でクリックボタンが離れた時
mousemove : 要素上でマウスが動かされたとき
click : 要素がクリックされたとき
dbclick : 要素がダブルクリックされたとき
keydown : 要素にフォーカスした状態で、キーボードのキーが押されたとき
keyup : 要素にフォーカスした状態で、キーボードのキーが離れた時
focus : 要素にフォーカスが当たった時
blur : 要素からフォーカスが離れた時
change : 入力内容が変更されたとき
resize : 要素がリサイズされたとき
scroll : 要素がスクロールされたとき

イベントハンドラ

sample.js
function() {
 //任意の処理
}

たとえば、

sample.js
$('#type').on('mouseover', function(){
 $('#type').css('color', '#ebc000')
})

のように使う。

メソッドチェーン

メソッドチェーンとは、「メソッドを鎖(チェーン)のようにつなげて記述し、実行するプログラム手法」です。書き方は、

sample.js
$('セレクタ').メソッドA().メソッドB().メソッドC()...

たとえば、

sample.js
$(function(){
 $('#type')
  .on('mouseover', function(){
   $('#type').css({
    color: '#ebc000',
    backgroundColor: '#ae5e9b'
   })
  .on('mouseout', function(){
   $('#type').css({
    color: '',
    backgroundColor: ''
   })
  })
  })
})

なお、実行の順番が結果に関係しないメソッドについては、順番を変えても同様の結果となる。
このメソッドチェーンのメリットは、

  1. $()関数の記述が一回で済むため可読性が上がる。
  2. プログラムの処理速度が上がる。

stopメソッド

実行中のアニメーションを途中でキャンセルするにはstop()メソッドを使います。

sample.js
stop( true | false )

参考資料

「jQuery最高の教科書」

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

初心者による JavaScriptの基礎

概要

JavaScriptの基本的な書き方や特徴をメモとして箇条書きのように書いていく。なにかあれば順次追加していく

文のルール

javascriptの文のルールとして、

  1. 文の末尾にセミコロンをつけることを推奨している。
  2. 大文字/小文字は厳密に区別される。

文字リテラル

文字リテラルは(')や(")で囲む必要があるが、ふたつ使いたい場合は、

sample.js
'He's a doctor!' -> 悪い例
"He's a doctor!" -> よい例

ただしエスケープ処理をすれば、大丈夫である。

sample.js
'He\'s a doctor' 

テンプレート文字列

テンプレート文字列を利用することで、以下のような文字列表現が可能になる。

  1. 文字列への変数の埋め込み
  2. 複数行にまたがる文字列
sample.js
let name = '斉藤'
let str = `こんにちは${name}さん。今日も天気いいですね!`
console.log(str)

分割代入(配列)

分割代入とは、配列/オブジェクトを分解し、配下の要素/プロパティ値を個々の変数に分解するための構文です。

sample.js
//従来のやり方
let data = [1, 2, 3]
let x1 = data[0]
let x2 = data[1]
let x3 = data[2]

//分割代入
let data = [1, 2, 3]
let [x1, x2, x3] = data

とてもすっきりしている。

分割代入(オブジェクト)

オブジェクトのプロパティを変数に分解することもできます。

sample.js
let book {
 title: 'javascript',
 price: 270
}

//分割代入
let {price, title, memo='なし'} = book

複雑な分解も可能です。

1. 入れ子となったオブジェクトを分解する

sample.js
let book = {
 title: 'javascript',
 price: 270,
 other: {
  author: '斉藤',
  logo: 'logo.jpeg'
 }
}

//分割代入
let {title, price, other: {author}} = book

2. 変数の別名を指定する

sample.js
let book {
 title: 'javascript',
 price: 270
}

//分割代入(名前変更)
let {price: value, title: name} = book

ショートカット演算(短絡演算)

sample.js
if(x === 1) {console.log("Hello!")}

x === 1 && console.log("Hello!")

上の二式は同じ挙動をする。if文を使わなくても簡単に同じことができる。よく使う印象。

for...of命令

「配列など」を順番に列挙するための一つの手段がfor...of命令だ。
配列などというのは、for...of命令は、Arrayオブジェクトやイテレーターやジェネレーターなども処理できるためである。これらを総称して列挙可能なオブジェクトという。

sample.js
for(仮変数 of 列挙可能なオブジェクト){
 //コード
}

for...inに対してfor...ofは値を順に列挙するのが特徴だ。

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

ExpoアプリでのStripe決済を整理+Paymentsを使ってみる

はじめに

直近の記事でWebViewを使用することによってExpoアプリをEjectせずにStripeを導入する方法を確認しました。
ここではもう一つの選択肢として、iOSにおいてEjectが必須であるPayments APIの方をまとめてみます。

Compatibilityを整理

なかなかややこしいので、まずはWebViewでの実装を含めて、各OS、機能で何ができて何ができないのかを整理しておきます。

checkout.js in WebView

Expoでの動作 → iOS/Android共に❌
サーバー側実装 → 不要
動的な金額設定 →
PSD2 → 不明(未準拠?)

Stripeのcheckout.jsは、カード入力フォームをWebページ上にモーダルで表示させて決済を導入できるライブラリです。
現在はレガシー扱いになっていて、下記のドキュメントのようにStripeがホスティングするページへリダイレクトさせる実装へと変更することが推奨されています。

Checkout migration guide
https://stripe.com/docs/payments/checkout/migration

今から導入する際には当然選択肢には入らないと思いますが、StripeをExpoで使用する際のライブラリとしてexpo-stripe-checkoutという一実装があり、こちらはこのcheckout.jsをWebViewで使用しています。
ちょっと試してみたところ、現在このcheckout.jsはブラウザがWebViewやスマホであると認識された場合に別タブでチェックアウト画面を開くような処理になっていて、うまく動作しませんでした。

Elements + Payment Intents in WebView

Expoでの動作 → iOS/Android共に⭕️
サーバー側実装 → 必要
動的な金額設定 →
PSD2 → 準拠可

Stripe Elementsはカード情報などの入力フォームを独自に作成するためのWeb向けAPIです。
ユーザーが入力したカード情報などに開発者がタッチできないよう設計されており、欧州の決済サービス指令(PSD2)に準拠したセキュアな決済フローを柔軟に実装できるようになっています。
Elementsの場合はPayment Intentという決済機能を使うのですが、これはサーバー側の実装が併せて必要になります。

Expoで使用する際はこちらの記事に簡単な例を載せたので、参照してください。
ejectなしのExpoアプリにStripe決済をWebViewを使って埋め込む
https://qiita.com/mildsummer/items/f95fd53864be6f14e3b0

Checkout + Sessions in WebView

Expoでの動作 → iOS/Android共に⭕️
サーバー側実装 → 必要(+静的HTMLホスティング)
動的な金額設定 →
PSD2 → 準拠可

現在のCheckoutは、redirectToCheckoutメソッドによってStripeがホスティングする決済フォームへリダイレクトする機能になっています。
通常はSessionをサーバー側で作成してからsessionIdを指定することでリダイレクトする感じになります。
そのほか、WebViewで使用する場合もコールバック先のHTMLはWeb上のどこかに用意する必要があります。
詳しくはこちらの記事の1.2.を参照してください。
ejectなしのExpoアプリにStripe決済をWebViewを使って埋め込む(Checkout編)
https://qiita.com/mildsummer/items/616677286e79cb8f8f75

Checkout クライアント専用組み込み in WebView

Expoでの動作 → iOS/Android共に⭕️
サーバー側実装 → 不要(静的HTMLホスティングは必要)
動的な金額設定 → 不可(SKU・個数を指定)
PSD2 → 準拠可

現状、Expoアプリに導入できるStripeの機能のなかで唯一サーバー側の実装が不要なパターンです。
ただし、SKUを指定する形になるため動的に金額を設定することはできません。これはセキュリティ上の制限ということだと思います。
Expoでの実装例はこちらを参照してください。

Expo Payments + Charge

Expoでの動作 iOS Android
Managed / Expo Client ⭕️(英語のみ)
Bare / Expo Client ⭕️(英語のみ)
Bare / Standalone ⭕️ ⭕️

サーバー側実装 → 必要
動的な金額設定 →
PSD2 → 未準拠

WebViewを使わず、Stripeのネイティブアプリ用ライブラリを使用したパターンです。
ExpoからはPayments API(expo-payments-stripe)を使用します。
Stripeの各OSライブラリをReact Nativeで使用できるようにしたものがtipsi-stripeで、これをExpoで使用できるようにしたものがexpo-payments-stripeと二重の依存関係になっているため、最新のバージョンが反映されるまでになかなか時間がかかりそうだなという懸念がありますが、一応使用できます。
ただし2020年1月現在のところPSD2に準拠したPaymentIntentsを使用できないため、欧州での使用が想定される場合は注意が必要です。
PSD2に準拠する必要がある場合は、Expoのフォーラムでも話題に上がっているので随時チェックしてみてください。
Tipsi-stripe SCA compliant Payment
https://forums.expo.io/t/tipsi-stripe-sca-compliant-payment/27422

表に書いた通り、iOSではBare Workflowでスタンドアロンビルドした状態のみで使用できます。
AndroidではEjectせずExpo Clientで使用できるのですが、今のところUI文言の設定にはリソースファイル(XML)の修正が必要で、故にカード入力UIを日本語に対応したい場合はeject/Bare Workflow化が必須です。

では、このPaymentsを導入する流れを説明していきます。

まずPaymentsを使ってみる

サーバー側の実装

ExpoのPaymentsではユーザーの入力したカード情報をトークン化する機能のみ使用できます。
このトークンはStripe Chargeの作成時にsourceオプションとして指定するものです。

このCharge作成の処理はサーバー側で行います。
上述したWebViewの記事と同じようにFirebase Cloud Functionsにデプロイするとしたら例えば以下のような感じになります。

functions/index.js
const functions = require('firebase-functions');

const app = require('express')();
const stripe = require('stripe')('sk_test_...'); // 秘密鍵
const cors = require('cors');
const bodyParser = require('body-parser');

app.use(require('body-parser').text());
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.post('/createCharge', async (req, res) => {
  const { token } = req.body;
  const result = await stripe.charges.create({
    amount: 2000, // 金額
    currency: 'jpy', // 通貨単位
    source: token, // トークンを渡す
    description: 'test charge', // 説明
  });
  res.json(result);
});

exports.api = functions.https.onRequest(app);

WebViewの記事と合わせて簡単にhttps.onRequestでexpressを使用していますが、勿論ちゃんとhttpsCallableやFirestoreとの連携などすることも可能です。

また、Firebase Cloud Functionsを使用する際に注意が必要な点があります。
Payment Intentsを使用した
1. サーバー側で決済情報を作成 → 2. クライアント側でカード情報と紐付けて決済完了
のパターンと違ってChargeの場合は
1. クライアント側でカード情報を作成(トークン化) → 2. サーバー側で決済
という流れになるので、サーバー側で冪等性を保証しないと同じカード情報を使って二重に決済されるということが容易に起こってしまいます。
詳しくはこちらのk-boyさんの記事が参考になります。

stripeをfirebaseで使うときidempotency_keyをつけよう
https://qiita.com/k-boy/items/6d8ce83084a0f49ab0d2

アプリ側を作成

ボタンをタップしたらカード入力フォームを表示するような画面を作ります。
android
expo-payments-stripeをインストールします。

$ npm install --save expo-payments-stripe

JS側はシンプルにApp.jsのみでこんな感じに。

App.js
import React, { Component } from 'react';
import { StyleSheet, Text, ActivityIndicator, TouchableOpacity, View } from 'react-native';
import { PaymentsStripe as Stripe } from 'expo-payments-stripe';

export default class App extends Component {
  state = {
    loading: false,
    succeeded: false
  };

  /**
   * カード入力画面を表示しCharge作成
   */
  charge = async() => {
    this.setState({ loading: true });
    try {
      await Stripe.setOptionsAsync({
        publishableKey: 'pk_test_...' // 公開鍵
      });
      const params = {
        // Only iOS support this options
        smsAutofillDisabled: true,
        requiredBillingAddressFields: 'full',
        prefilledInformation: {
          billingAddress: {
            name: 'Hanako Yamada',
            line1: 'line1',
            line2: 'line2',
            city: 'Yokohama',
            state: 'Kanagawa',
            country: 'JP',
            postalCode: '2440000',
            email: 'test@test.com',
          }
        }
      };
      const source = await Stripe.paymentRequestWithCardFormAsync(params);
      const result = await fetch('https://[リージョン名]-[プロジェクト名].cloudfunctions.net/api/createCharge', { // 先ほどのAPIを叩く
        method: 'POST',
        headers: {
          'Content-type': 'application/json'
        },
        body: JSON.stringify({ token: source.tokenId })
      });
      this.setState({ loading: false, succeeded: result.status === 200 });
    } catch (e) {
      console.log(e);
      this.setState({ loading: false });
    }
  };

  /**
   * Chargeをキャンセル
   */
  cancel = () => {
    this.setState({ isCharge: false });
  };

  render() {
    const { loading, succeeded } = this.state;
    return (
      <View style={styles.container}>
        <Text style={styles.title}>支払いのテスト</Text>
        <TouchableOpacity onPress={this.charge} disabled={succeeded || loading}>
          <View
            style={[
              styles.button,
              succeeded && styles.succeededButton
            ]}
          >
            {loading && <ActivityIndicator color="#ffffff" style={styles.buttonIndicator} />}
            <Text
              style={[styles.buttonText, succeeded && styles.succeededButtonText]}
            >
              {succeeded ? '支払いが完了しました' : '支払い'}
            </Text>
          </View>
        </TouchableOpacity>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    height: '100%',
    width: '100%',
    backgroundColor: '#ffffff',
    alignItems: 'center',
    justifyContent: 'center'
  },
  title: {
    position: 'relative',
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 24
  },
  modal: {
    justifyContent: 'flex-end',
    margin: 0
  },
  modalInner: {
    height: '70%',
    backgroundColor: '#ffffff'
  },
  button: {
    position: 'relative',
    width: 240,
    height: 50,
    borderRadius: 25,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'orange'
  },
  succeededButton: {
    borderColor: 'orange',
    borderWidth: 2,
    backgroundColor: 'transparent'
  },
  buttonText: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#ffffff'
  },
  succeededButtonText: {
    color: 'orange'
  },
  buttonIndicator: {
    position: 'absolute',
    top: 0,
    left: 0,
    width: 50,
    height: 50,
    justifyContent: 'center',
    alignItems: 'center'
  }
});

paymentRequestWithCardFormAsyncに渡すオプションはiOSのみ有効です。住所などの入力を必須としたり、UIの色を変更できたりします。

まずはこれをejectせずAndroidで確認してみます。ボタンをタップすると、
android
カード入力画面がモーダルで表示されました。
テストカードを入力し、
android
android
先ほど用意したサーバー側の処理を含めて成功したら、ダッシュボードを確認。
dashboard.png
無事決済が完了しています。

Bare WorkflowでPaymentsを確認

さて、ここまでは簡単なのですが、前述した通りiOSでは完全にejectが必須、あるいは最初からBare Workflowで使用することになります。また、Androidのカード入力画面を日本語化するためにもejectが必要になります。

ということで、ここからはBare WorkflowやExpoKitを使用したReact Nativeプロジェクトを前提とします。
それに伴ったビルドの流れやデバッグなどはこの記事の要旨から外れるので割愛するとして、iOSでの動作確認・設定項目、Androidでの言語設定の方法を説明していきます。

iOSで確認

Expo Clientで表示するとsetOptionsAsyncの時点でTypeError: Cannot read property 'init' of undefinedというエラーが発生すると思いますが、eject後、スタンドアロンビルドしてXCodeからビルドして確認するとすんなり使用できます。

ボタンをタップすると、
PNGイメージ 34.png
カード入力画面が立ち上がります。住所欄にはpaymentRequestWithCardFormAsyncのオプションで渡した内容が表示されています。
PNGイメージ 36.png
テストカードを入力し、「Done」をタップすると、
PNGイメージ 37.png
問題なく決済ができました。
PNGイメージ 38.png

UIの色を変更

iOSのカード入力UIの色はオプションで変更することができます。

App.js
const params = {
  // Only iOS support this options
  smsAutofillDisabled: true,
  requiredBillingAddressFields: 'full',
  prefilledInformation: {
    billingAddress: {
      name: 'Hanako Yamada',
      line1: 'line1',
      line2: 'line2',
      city: 'Yokohama',
      state: 'Kanagawa',
      country: 'JP',
      postalCode: '2440000',
      email: 'test@test.com',
    }
  },
  theme: { // UIの色を変更
    primaryBackgroundColor,
    secondaryBackgroundColor,
    primaryForegroundColor,
    secondaryForegroundColor,
    accentColor,
    errorColor
  }
};
const source = await Stripe.paymentRequestWithCardFormAsync(params);

とりあえずaccentColorをブランドカラー的な色に変更するだけでも十分かなと思います。
PNGイメージ 39.png

日本語対応

上の画像のようにデフォルトではUIが英語になっているので、これを日本語に変更してみます。
tipsi-stripeの下記のissueを参考にします。
https://github.com/tipsi/tipsi-stripe/issues/97

iOSではXCodeのプロジェクト設定→「info」タブ→「Localizations」を開き、
ExpoPaymentsStripeExample_xcodeproj.png
1. Use Base Internationalizationをチェック(通常チェックされているはず)
2. 「+」で「Japanese」を選択し、「Finish」

このようにUIが日本語になりました。
PNGイメージ 40.png

住所入力無しの場合

住所入力を必要としない場合はrequiredBillingAddressFieldsを指定せず、

App.js
const params = {
  smsAutofillDisabled: true,
  theme: {
    accentColor: 'orange'
  }
};
const source = await Stripe.paymentRequestWithCardFormAsync(params);

このような見た目になります。
PNGイメージ 41.png

Androidの設定

日本語対応

Androidでのカード入力画面はnode_modules/expo-payments-stripe/android/src/main/res/内のリソースをandroid/app/src/main/res/内のリソースで上書きするような形で設定が可能です。
上記のissueにacro5piano氏が紹介している例の通りにandroid/app/src/main/res/values/strings.xmlを以下のように変更してみます。

android/app/src/main/res/values/strings.xml
<resources>
    <string name="app_name">ここはアプリ名</string>
    <string name="gettipsi_card_number">カード番号</string>
    <string name="gettipsi_save">保存</string>
    <string name="gettipsi_card_cvc">確認番号(CVC)</string>
    <string name="gettipsi_google_pay_unavaliable">この端末では、 Google Pay はご利用になれません。</string>
    <string name="gettipsi_user_cancel_dialog">キャンセルしました。カードは追加されていません。</string>
    <string name="gettipsi_card_enter_dialog_title">カード番号を入力して下さい</string>
    <string name="gettipsi_card_enter_dialog_positive_button">完了</string>
    <string name="gettipsi_card_enter_dialog_negative_button">キャンセル</string>
    <string name="gettipsi_card_number_label">カード</string>
    <string-array name="gettipsi_currency_array">
        <item>通貨 (任意)</item>
        <item>指定しない</item>
        <item></item>
    </string-array>
</resources>

ビルドして確認すると、このように文言が指定したものに変更できました。
android

色の変更など

colors.xmlを追加し、アクセントカラーを変更してみます。

android/app/src/main/res/values/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorAccent">#FFA500</color>
</resources>

ボタンの色が変わります。iOSと違って、カード画像の色は変わりませんでした。
android

PNG画像を使っているので動的な変更は難しそうです。
同じ要領でandroid/app/src/main/res/drawable/stp_card_form_front.pngの画像を変更してみると、UIに反映されます。
android

tipsi-stripe関連で発生したエラーとその対応(Android)

ビルド時にCould not resolve com.github.tipsi:CreditCardEntryというエラーが発生しました。
JitPackというサービスが必要なようなので、build.gradleに追加します。

android/build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext {
        buildToolsVersion = "28.0.3"
        minSdkVersion = 21
        compileSdkVersion = 28
        targetSdkVersion = 27
        supportLibVersion = "28.0.0"
    }
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        mavenLocal()
        google()
        jcenter()
        maven {
            // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
            url "$rootDir/../node_modules/react-native/android"
        }
        maven {
            // Android JSC is installed from npm
            url("$rootDir/../node_modules/jsc-android/dist")
        }
        maven { url 'https://www.jitpack.io' } // この行を追加
    }
}


task wrapper(type: Wrapper) {
    gradleVersion = '4.7'
    distributionUrl = distributionUrl.replace("bin", "all")
}

終わり

以上です。ユースケースに従って実装方法を検討してみてください。

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

初心者による Array/Map/Setオブジェクト

Arrayオブジェクト

Arrayオブジェクトは、配列型の値を扱うためのオブジェクトで、配列に対して要素の追加/削除、結合、並べ替えを行うための機能を提供する。配列を生成するには原則、配列リテラルを利用する。

sample.js
let array = []

のように定義する。

Arrayオブジェクトの主なメンバー

Arrayオブジェクトで利用できるメンバーは以下のようなものがある。

length : 配列のサイズ
isArray(obj) : 指定したオブジェクトが配列にあるか
toString() : 「要素 , 要素 , ...」の形式で文字列に変換
toLocaleString() : 配列を文字列に変換(区切り文字はロケールによって変化)
indexOf(elem[, index]) : 指定した要素に合致した最初の要素のキーを取得(indexは検索開始位置)
lastIndexOf(elem[, index]) : 指定した要素に合致した最後の要素のキーを取得(indexは検索開始位置)
entries() : すべてのキー/値を取得
keys() : すべてのキーを取得
values() : すべての値を取得

concat(ary) : 指定配列を現在の配列に連結
join(del) : 配列内の要素を区切り文字delで連結
slice(start[, end]) : start + 1 ~ end番目の要素の抜出し
splice(start, cnt[, rep[, ...]]) : 配列内の start + 1 ~ start + cnt番目の要素をrep...で置き換える
from(alike, [map[, this]]) : 列挙可能なオブジェクトを配列に変換する
of(e1...) : 可変長引数を配列に変換
copyWithin(target, start[, end]) : start + 1 ~ end番目の要素をtarget + 1番目からの位置にコピー
fill(val, start[, end]) : 配列内のstart + 1 ~ end番目の要素をvalで置き換え

pop() : 配列末尾の要素を取得、削除
push(data1[, data2, ...]) : 配列末尾に要素を追加
shift() : 配列先頭の要素を取得し、削除
unshift(data1[, data2, ...]) : 配列先頭に要素を追加

reverse() : 逆順に並べ替え
sort([fnc]) : 要素を昇順に並べ替え

//コールバック関数を使ったメソッド
forEach(fnc[, that]) : 配列内の要素を関数fncで順に処理
map(fnc[, thst]) : 配列内の要素を関数fncで順に加工
every(fnc[, that]) : 配列内のすべての要素が条件fncに合致するか
some(fnc[, that]) : 配列内のいずれかの要素が条件fncに合致するか
filter(fnc[, that]) : 条件fncで合致した要素だけで配列を生成
find(fnc[, that]) : 関数fncが初めてtrueを返した要素を返す
findIndex(fnc[, that]) : 関数fncが初めてtrueを返した要素のインデックス番号を取得
reduce(fnc[, init]) : 隣同士の要素を左から右へ関数fncで処理して単一の値にする
reduceRight(fnc[, init]) : 隣同士の要素を右から左へ関数fncで処理して単一の値にする

かなり多くのメソッドがあるが、どれもC++などにもライブラリで備えてあるものである。違いとしては、Arrayオブジェクトはスタック/キューの区別がないことが挙げられる。コールバック関数を使ったメソッドは使用頻度が高い印象。

Mapオブジェクト

Mapオブジェクトは、キー/値のセット、いわゆる連想配列を管理するためのオブジェクトです。

sample.js
//mapの定義
let m = new Map()

//アクセス
for(let [key, value] of m){
 console.log(key)
 console.log(value)
}

Mapオブジェクトの主なメンバー

Mapオブジェクトで利用できるメンバーは以下のようなものがある。

sample.js
size : 要素数
set(key, val) : キー/valの要素を追加。重複時は上書き。
get(key) : 指定したキーの要素を取得
has(key) : 指定したキーの要素があるのかを判定
delete(key) : 指定したキーの要素を削除
clear() : すべての要素を削除
keys() : すべてのキーを取得
values() : すべての値を取得
entries() : すべてのキー/値を取得
forEach(fnc[, that]) : マップ内の要素を関数fncで順に処理

Setオブジェクト

Setオブジェクトは、重複しない値の集合を管理するためのオブジェクトです。

sample.js
//Setの定義
let s = new Set()

//アクセス
for(let val of s){
 console.log(val)
}

Setオブジェクトの主なメンバー

Setオブジェクトで利用できるメンバーは以下のようなものがある。

sample.js
size : 要素数
add(val) : 指定した値を追加
has(key) : 指定した値の要素があるのかを判定
delete(key) : 指定した値の要素を削除
clear() : すべての要素を削除
values() : すべての値を取得
entries() : すべてのキー/値を取得
forEach(fnc[, that]) : マップ内の要素を関数fncで順に処理

参考資料

山田祥寛様 「javascript本格入門」

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

クライアントサイドだけで日本語PDFを出力したときの文字化け・改行不具合をpdfmake最新版で再ビルドして直した話

はじめに

  • 常用文字にサブセット化を行ったものを id:naoa_yさんが作ってくれているのでそれを使っても良い
    • ビルド後のファイル(vfs_fonts.js)で 5MB -> 2.3MBぐらいに減ってる
    • ただしbuildが5年前なのでpdfmakeのバージョンが0.1.20と古い
    • 最新は0.1.63 (released this on 11 Dec 2019)
  • 上記のpdfmake@0.1.20だと日本語化したときの文字改行(word wrap)が適切にうごかない&改行されたとき文字化けする不具合があった
    • 私の場合、可変長の文字を動的に組み込む必要があり、自動改行はどうしても必要だった。1行に収まる短文のみであれば問題ないかと
  • 最新だとなおるかもしれないと、pdfmakeを最新にして使いたかったので、buildし直した。結論、治った!
  • Vueと書いてるけど、フォント差し替えるまでは共通だとおもう
  • @watameさんの記事を参考にしました。ありがとうございます。

TL;DR

  • このリポジトリにある、build/pdfmake.min.js, build/vfs_fonts.jsを使えばよいです。2つ合わせると10MB程度になります
  • https://github.com/yazashin/pdfmake/tree/0.1.63-ja
  • フォントサイズを減らしたい・違うフォントにしたいって人は下記を読みましょう

日本語対応pdfmakeをビルドする

fork&cloneする

cloneした後branchを0.1系に切り替える

  • "This is unstable master branch for version 0.2.x, for stable version 0.1.x see branch 0.1."とのこと

日本語フォントを入手する

  • ここは好み・用途によるとおもいますが、私のケースではこのフォントがマッチしました
  • https://ipafont.ipa.go.jp/#jp
  • IPAexゴシックのみ利用(等幅フォント)
  • メリデメを考えた時、メリットが勝ったのでこれを選択
    • 用途として、珍しい漢字もつかわれるのでサブセット化したとき文字化けの発生が怖い
    • pdfmakeとvfs_fontsで合計して10MBぐらいになる
      • pdfmake.min.js 1.2MB / vfs_fonts.js 8.1MB
      • 印刷ページごとに都度読み込みタイプだと辛い印象がある
      • vueのようなSPAでindex.htmlにscriptタグでglobalに登録してしまえば、大きな問題にはならないって考えて進めた
      • ぶっちゃけ最近はネット回線高速なので、SPAで初回ロード時に1回発生する10MB程度ならほとんど気にならないと思う
        • 安定したらServiceWorkerのキャッシュ対象にしてしまうのもありだとおもう
      • SEO気にする必要がない業務系サービスなのでこの判断ができている。っていう前提があります。
        • むしろSEO気にするサービスで、SPA & PDF生成が必要って、かなりニッチだとはおもうけどw

フォントを入れ替える

  • before
    image.png

  • after
    image.png

vfs_fonts.jsの更新

  • buildFontsを実行
$ yarn install
  ....
success Saved lockfile.
✨  Done in 216.37s.

$ ./node_modules/.bin/gulp buildFonts
[10:45:48] Using gulpfile ~/workspace/pdfmake/gulpfile.js
[10:45:48] Starting 'buildFonts'...
[10:45:48] Finished 'buildFonts' after 189 ms
  • pdfmakeへフォントを反映
$ yarn run build
...
WARNING in webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/
[10:50:16] Finished 'build' after 23 s
✨  Done in 24.49s.
  • warning出てるけど無視して進める

vfs_fonts.jsの確認

  • buildフォルダにあるvfs_fonts.jsがこうなっていれば大丈夫 build.png

Vue(typescript)で確認

  • 今回つくったpdfmake & vfs_fonts は、npmとして登録はせず、publicなフォルダに静的jsとして利用する
    • npmにするとdeploy時にciでビルドするとき、フォントのビルドから必要になるとか、そこらへん面倒でこうした
  • vue-cliのversionによって構成かわりますが、VUE_PROJECT_DIR/public/static-js/配下においた

index.htmlに登録

  • ポイントして、 deferを忘れずにつけましょう。10MB近いjsの読み込みでHTMLのパースが止まってしまいUXに影響がでます
index.html
 ... ~~~ ...
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
  <script defer src="/static-js/pdfmake.min.js"></script>
  <script defer src="/static-js/vfs_fonts.js"></script>
</html>

Vueの実装

  • Vueで使うときはこんなかんじ。手打ちなので間違ってるかもしれません。
pdf.vue
declare var pdfMake: any;
@Component({})
export default class PrintPdf extends Vue {
    createPdf() {
        pdfMake.fonts = {
            IPA_gothic: {
                normal: 'ipaexg.ttf',
                bold: 'ipaexg.ttf',
                italics: 'ipaexg.ttf',
                bolditalics: 'ipaexg.ttf',
            }
        };
        const defaultStyle = 'IPA_gothic';
        const docDefinition = {
            content: 'Hello 日本語!',
            defaultStyle: {
                font: defaultStyle
            }
        }
        pdfMake.createPdf(docDefinition).print();
    }
}
  • pdfmake 0.1.63 & IPAゴシック で生成したPDF
  • なお、文章はジェネレータでつくったので意味をなしてません。
    image.png

  • 文字改行時の文字化け不具合がなおって本当に良かった..

Thanks

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

じゃんけんプログラムを改変してた

前回のあらすじ

・if文とSwitch文を使ってじゃんけんプログラムを組んだ

今日やったこと

・関数定義して、ソースコードをスッキリさせた
・While文を使って、連勝している間はじゃんけんが続き、最終的にレコードが出るように改変した。
(今回の教科書『いちばんやさしいJavaScriptの教本』)

今回のエラー

・改変前プログラムをコピペしてて、関数名を古いままにしてたのでエラーが起こった

感想

関数って、ソースコードがスッキリするし1つ1つの部分がわかりやすくなるし、すごく便利な存在なんだな~と思った。

次回の目標

HTMLとCSSとJSを結びつけて活用できるようになるぞ

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

Node.js の基礎とそのフレームワーク Express

Node.js とは何なのか

Node.js とは、簡単にいうと JavaScript をサーバーサイドで実行させてくれる存在です。フロントエンドも、バックエンドも1つの言語で実行でき、WEBサービス、スマートフォンアプリ、IoT関連の開発にも使用することができるみたいです。

インストール

まず Node.jsをインストールしていない人は、[Node.js公式サイト](https://nodejs.org/en/) から、LTS版をダウンロードしてインストール。

『node』 でJavaScriptを実行させる

コマンドラインでindex.jsが格納されているフォルダにpwdで移動し、そのフォルダ内のindex.jsを実行する。

node index.js

nodeだけでEnter押すと、JavaScriptコンソールで色々なコードを試せるようになるみたい。やめるときは .exit と入力するか、control + C を2回押すと終了します。

Node.js のいろんなAPI

Node.jsには様々な機能があり、その一覧が公式サイトのdocumentationで見れます。
https://nodejs.org/dist/latest-v12.x/docs/api/

APIの1つ。 『File System』

FileSystemは、ローカルファイルとかにアクセスすることができるAPI。

今回は、Node.js の File System を使用して、ローカルファイルのコピーを使用します。

FileSystemを読み込む

qiita.js
//jshint esversion:6

const fs = require("fs");

※ const とは、var のようなもの。しかし、var が後から変更できるのに対して、constは後から変更できることができず、一度データを格納(代入?)するとずっとそのまま。

※コメントがないと、"const" is available in ES6 というエラーメッセージが出てきますが、コメントで//jshint esversion:6と記載することでエラーメッセージを回避することができます。

ファイルのコピーを作成する

qiita.js
//jshint esversion:6

const fs = require("fs");

fs.copyFileSync("file.txt","copyfile.txt");

//fs.copyFileSync("コピーするファイル","コピーされたもののファイル名");

※ file.txtのコピーを作成して、copyfile.txtを作成します。って意味。 ちなみに、すでにcopyfile.txtという名前のファイルが存在していた場合、上書き保存されるので注意。

コマンドラインで、実行する

node qiita.js

※今回は、qiita.js というファイルにJavaScriptコードを記載していたので、そのファイルをコマンドライン上で実行する。

Expressフレームワーク

node.js をより使いやすくしたのがExpress フレームワークらしい。めっちゃざっくりだけど。

Expressのインストール

スクリーンショット 2020-01-17 21.49.43.png

retrieved from Express.jp 公式サイト
https://expressjs.com/ja/starter/installing.html

最後のところは『 npm install express 』だけでOKみたい。

Express をjsファイルで使用できるようにする

requireでExpressをロードし使用できるようにします。ついでにexpress()もappに格納します。

qiita.js
const express = require("express");

const app = express();

Expressでサーバーを作成する

ここが割と理解に苦しんだところ。

手順

========================
(JavaScript上)
1.まずExpressをロード
2.アクセスされた時のRespondを考える(ページ別)
3.listenでサーバーを指定
(コマンドラインにて)
4.node server.js で実行
(ブラウザ上にて)
5.http://localhost:3000/と入力し、サーバーに接続
6.respondの処理が実行される
(コマンドラインにて)
7.control + C で終了する

========================

これが一応の流れで、下記が実際のコード。

server.js
// jshint esversion:6

// expressをロード
const express = require("express");
const app = express();

// localhost:3000 にアクセスされた時のRespond
app.get("/",function(request, respond){
  //ここでconsole.log(request);とするとrequestの内容みれる
  respond.send("Hello World");
});


// localhost:3000/profile にアクセスされた時のRespond
app.get("/profile", function(request, respond){
  respond.send("My name is Kibinag0. I'm from Japan.");
});

//listenで待ち受け状態にする
 app.listen(3000, function(){
   console.log("Server started on port 3000");
 });


・Request

ブラウザ?からサーバーにリクエストされる情報。

・Respond

 サーバーにリクエストが来た時に、反応して実行する処理。

ちなみにサーバーにはスレッドモデル(Apache等)とイベントループ(Node.js等 シングルスレッドともいう)という種類があり、それぞれの違いはこちらが分かりやすかったため、参照。
https://dotinstall.com/lessons/basic_nodejs/26202

・listen()

listenで待ち受け状態にする。このlistenがあることによってrequestを受け取ることができるようになる。

『res.sendFile』を使用してHTMLファイルでRespondする

今までは、"Hello world"とかいう文字列でRespondしていましたが、今回はres.sendFileを使用してHTMLファイルでRespondします。

server.js
// リクエストがあったら、index.htmlファイルをrespondする
app.get("/", function(req, res){

  res.sendFile(__dirname + "/index.html");

});

(__dirname + "/index.html")は、dirnameをちゃんと取得して、そのindex.htmlがどのディレクトリに格納されているのかを教えてあげる必要があるんですね。

body-parserを使用してformデータを取得する

HTMLファイルでFormを使用して、ユーザーにデータを入力してもらう。そして、そのデータを使用して何か処理を行う場合の方法です。

事前にbody-parserをインストール

npm install body-parser

HTMLでフォームを作成

index.html
  <h1>Calculator</h1>
  <form class="" action="/" method="post">
    <input type="text" name="num1" placeholder="First Number">
    <input type="text" name="num2" placeholder="Second Number">
    <button type="submit" name="submit">Calculate</button>
  </form>

このフォームをサーバー側で

calculator.js
// jshint esversion:6

// ロードする
const express = require("express");
const bodyParser = require("body-parser");
const app = express();

// body-parserの使用
app.use(bodyParser.urlencoded({extended: true}));

// index.html でrespondする
app.get("/",function(req,res){
  res.sendFile(__dirname + "/index.html");
});


// index.htmlのフォームでpostされた部分
app.post("/",function(req, res){

 // bodyの中のnum1, num2を取得する
  var num1 = Number(req.body.num1);
  var num2 = Number(req.body.num2);

  var result = num1 + num2;

  res.send("The result of the calculation is " + result);
});

app.listen(3000);



Udemy AngelaさんのWEB DEVELOPMENT COURSE 『206. Processing Post requests with body-parser』より

サーバー更新自動化『nodemon』のすすめ

ちなみに、上記だとserver.jsファイルを更新するたびにコマンドラインでサーバー終了して、もう一回立ち上げることになります。それって結構めんどくさいですよね。

そんな人のために、Udemy講師のAngelaさんが役立つツールnodemonを教えてくれました。
Angelaさんの講義はこちら→ Udemy Bootcamp web development

nodemonを使うと、server.jsファイルを上書き保存するたびに検知してくれて、自動でサーバーを更新し、反映させてくれる優れものです。

https://nodemon.io/

インストール

インストールの仕方はコマンドラインで

npm install -g nodemon

と記載するだけ。

※permission error が出たら

sudo npm install nodemon -g

で対応。

使い方

nodemon server.js

とコマンドラインで記載すると、server.jsファイルの更新を自動反映してくれます。

以上 Node.jsとExpress。
結構難しいかったので、追加、修正を随時していきます。。

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

初めての Azure Bot Service - Sample code と local test 編 -

はじめに

今回は Azure Bot Service を使用して連続した対話に対応できる Bot を作成します.
Azure Bot Service を初めて使用する人を(できる限り)想定してます.
(熟練の方々からしたら退屈するかと思います.)

また,今回は Bot の sample を local test するところまでで
Azure への Deploy については次回行います.

(訂正や指摘などあれば,温かく教えていただけると助かります.)

私の開発環境

  • PC : surface laptop2 (windows 10)
  • Editor : VS Code

PC に関しては何でも大丈夫です.
Editor に関しては 本記事を参考にされる場合は VS Code をご使用ください.
(後程お話しますが, Azure への deploy 時に VS Code の Extension を使用しています)

Azure Bot Service の概要

Azure Bot Service とは,Microsoft 社のクラウドサービスである Azure のサービスの1つです.
公式の document は以下をご参考ください.

Azure Bot Service のリソースを作成すると,セットで Azure App Service のリソースが作成されます.App Service とは同じく Azure のサービスの 1 つで,Web Application を作成することの出来るサービスです.

Bot の基本

まずは,Bot が初めての方,Auzre Bot Service に初めて触れる方のためにいくつかの用語や実装法の紹介をします.

専門用語

Azure Bot Service を使用する上で参考になる用語をいくつか先にご紹介しておきます.

  • channel
    • Bot への通話手段.窓口のようなイメージ.
    • User は channel を通じて Bot App と会話をします.
    • Teams や LINE, Slack,Web chat などがあります.
    • channel によって仕様が異なるため,実際の Bot 開発では channel が何かも大事.
  • connecter
    • channel と Bot アプリを繋ぐ.
    • 実は Azure Bot Service の実態はこの部分(だと思われる)
  • Bot App
    • 実際に処理を行う部分.
    • 実態としては,一般的な Web App と変わらない
    • Azure Bot Service の場合は App Service に相当します

bot archtecture.png

ご存知の方もいらっしゃるかもしれませんが,Azure Bot Service 自体は Bot Application ではありません.channel を通してきたメッセージを Bot へと伝える connecterの役割を担っています.

Bot の実装方法

Bot アプリを作成する場合,Line や Slack などの channel ではなく,実装の観点で話すと2つの実装方法があります.

  • function 型
    • AWS lambda や Azure Function などの Serverless Function を活用した場合
    • 非常にシンプルな作り
    • Function を使用した回数だけ課金対象になるので使用頻度によっては非常に安価
  • Server 型
    • 今回のAzure Bot Service(厳密には Web App Service)のような場合
    • serverを起動している間機能する
    • State(変数の値など)を保持することができる

Azure Bot Service を使用した場合は,下の server 型になります.

function 型では リクエスト(Bot の場合は Bot への呼びかけ)が発生する度に function が実行されます.一方,server 型では server が起動している間のみ応答を返します. これらの特性の大きな違い(の 1 つ)は server側で変数の値などを保持してステートフルな会話が可能なことです.

ステートフルな会話とは簡単に言うと一連の流れを記憶した状態で会話ができるということです.
Serverless Function であれば,基本的には 「1 Question : 1 Answer」 であるのに対して,ステートフルな実装であれば「 n Question : m Answer」と複数の情報から返答を返すことができます.
とりあえずここでは,ざっくりと下図のようなイメージだけで大丈夫です.
(私も厳密な定義は微妙です(笑))

state-full bot.png

sample の実行

それでは実際に sample を動かしつつ確認していきます.

sample コードの取得

以下の Github から sample コードを取得します.

Azure Bot Service の sample Code (Github)

今回は連続した会話フローのsample (Node.js版) を使用します.
先ほど取得したコードのうち,以下の directory を開いてください.

/BotBuilder-Samples/samples/javascript_nodejs/05.multi-turn-prompt/

少しコードを見てみましょう.
プログラムを実行すると index.js が実行されます.
まずは index.js をみてみます.一部抜粋したものを記載しておきます.

index.js
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const restify = require('restify');
const path = require('path');

// Import required bot services.
// See https://aka.ms/bot-services to learn more about the different parts of a bot.
const { BotFrameworkAdapter, ConversationState, MemoryStorage, UserState } = require('botbuilder');

// Import our custom bot class that provides a turn handling function.
const { DialogBot } = require('./bots/dialogBot');
const { UserProfileDialog } = require('./dialogs/userProfileDialog');

// Read environment variables from .env file
const ENV_FILE = path.join(__dirname, '.env');
require('dotenv').config({ path: ENV_FILE });

/**********
  中略
**********/

// Define the state store for your bot.
// See https://aka.ms/about-bot-state to learn more about using MemoryStorage.
// A bot requires a state storage system to persist the dialog and user state between messages.
const memoryStorage = new MemoryStorage();

// Create conversation state with in-memory storage provider.
const conversationState = new ConversationState(memoryStorage);
const userState = new UserState(memoryStorage);

// Create the main dialog.
const dialog = new UserProfileDialog(userState);
const bot = new DialogBot(conversationState, userState, dialog);

// Create HTTP server.
const server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function() {
    console.log(`\n${ server.name } listening to ${ server.url }.`);
    console.log('\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator');
    console.log('\nTo talk to your bot, open the emulator select "Open Bot"');
});

// Listen for incoming requests.
server.post('/api/messages', (req, res) => {
    adapter.processActivity(req, res, async (context) => {
        // Route the message to the bot's main handler.
        await bot.run(context);
    });
});

前半部分では必要な module を import したり,
sample プログラムの別の js ファイルでインスタンスを作成しています.

HTTP server を create しているところでは,PORT の設定なんかもしていますね.

また,最後の部分で post する際のエンドポイントを設定しています.
今回はしませんが,Proactive なメッセージ(push 通知など)を実装する際には,
同様の方法で Proactive 用のエンドポイントを用意することもあります.

続いて,実際の会話フローの制御をしているプログラムを確認しましょう.
/dialogs/userProfileDialog.js を開いてください.一部抜粋したものを記載しておきます.

userProfileDialog.js
constructor(userState) {
        super('userProfileDialog');

        this.userProfile = userState.createProperty(USER_PROFILE);

        // 典型的な会話のパターンを用意してくれている
        this.addDialog(new TextPrompt(NAME_PROMPT));
        this.addDialog(new ChoicePrompt(CHOICE_PROMPT));
        this.addDialog(new ConfirmPrompt(CONFIRM_PROMPT));
        this.addDialog(new NumberPrompt(NUMBER_PROMPT, this.agePromptValidator));

        // ここで連続する会話を追加している
        this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
            this.transportStep.bind(this),
            this.nameStep.bind(this),
            this.nameConfirmStep.bind(this),
            this.ageStep.bind(this),
            this.confirmStep.bind(this),
            this.summaryStep.bind(this)
        ]));

        this.initialDialogId = WATERFALL_DIALOG;
    }

TextPromptNumberPrompt はその名の通り,テキストや数値入力などの典型的な会話のパターンを実装するために用意されています.

そして,抜粋した code の後半部分にあるWatarfallDialog に様々な dialo を add することで,add した dialog が一連の会話として扱われます.ここで add されている dialog は抜粋部分より後ろの sample code で上記の Prompt を継承して実装されたものです.

local test

まずは Azure に deploy(ざっくり言うとプログラムを Azure 上で動作させる) せずに,local で動作テストできるようにします.以下のリンクより Bot Framework Emulator の exe ファイルを取得して実行します.

Bot Framework Emulator

ちょっと分かりにくいですが,ページ中頃にある以下の画像ようなファイル群から自分に適したものを入手してください.

BotEmulator_download.png

上 4 つのどれかで大丈夫なはずです.

Bot Framework Emulator が起動できること(アプリが実行できて window が開くこと)を確認したら,sample program のある場所で command prompt やら Power Shell やらのコマンドツールを開きましょう.何かしらのコマンドツールを開けたら,npm installnpm startを実行してください.

すると,以下のような画面になります.

npm start.jpg

この画面になり,Bot App が Listen 状態になったことを確認したら,
先ほどの Bot Framework Emulator を実行してOpen Botを押してください.
そして,URL の欄にhttp://lovalhost:3978/api/messages と入力します.
これは sample code で index.js の最後に書かれている server.post のエンドポイントです.
このポイントに http post リクエストを送ることで Bot と会話します.

Bot Emulator OpenURL.png

こんな感じですね!入力したら Connect してください.

無事にsample を実行できたら,何か話しかけてみましょう.
下の gif のように会話ができると思います.

Bot-Framework-Emulator-2020-01-17-11-37-53_Trim.gif

ちなみに,Bot を停止させたい時は shell の画面で ctr+c を押してください.

次の内容

少し長くなりそうなので今回はここまでで終了とします.
もう1回くらいは書こうと思うので,予定している内容を書いておきます.

  • Dialog の分岐
    • 今回は決まった会話内容でしたが,次回はもう少し複雑に会話を分岐させます.
  • Azure への Deploy
    • 今回は localtest で終わりましたが,次回は実際に Azure に Deploy します.
  • channel の設定
    • とりあえず Teams との連携でも目指します.

(よくよく考えたら今回まだ Azure Bot Service 使ってない説)

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

`let`にあんまり知られてなさそうな性質が結構あった話

前書き

A
for (var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, 100);
}

// 5 5 5 5 5
B
for (let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, 100);
}

// 0 1 2 3 4

この2つのコード、varletの違いとしてそこそこ有名なものだと思います。

この動作自体に文句はありませんし、自分もそういうもんだと思って使っていたのですが・・・

よくよく考えるとBのコード上では

  1. console.log(i)letで宣言された変数iを参照している
  2. letで宣言された変数iは、for文によってインクリメントされている
  3. console.log(i)が実行されるのは、forループが完了した後である

という処理になっているため、Aのコードと同じ結果になるはずです。

MDNにあるようなletの性質だけでは、
この部分の説明がつかなくて気になってしまったというのが発端で、
色々試していた+調べていたらそこそこな量になったのでまとめました。

ただ、知られてなさそうな性質として気になったのが全て
forと組み合わせた時に表れる性質なので
実用性はそんなにないと思います。

よく知られているであろう性質

ブロックスコープである

{
    let a = 1;
    console.log(a); // 1
}
console.log(a);

// Uncaught ReferenceError: a is not defined

let宣言するとスコープがブロック内に制限されるやつです。

どこのサイトでも載ってる基本中の基本。

同一ブロック内での再宣言不可

{
    let a = 1;
    let a = 2;
}

// Uncaught SyntaxError: Identifier 'a' has already been declared

let宣言すると、同一スコープで再宣言ができません。
これはvarでの再宣言でも同じくエラーになります。

{
    let a = 1;
    {
        let a = 2;
        console.log(a);
    }
    console.log(a);
}

// 2 1

のように、ブロック内の別ブロックであれば
再宣言ではなく新規の変数宣言扱いになるので、エラーは出なくなります。

これも基本。

for文のループブロックは、それぞれが別のブロックスコープになる

for (let i = 0; i < 5; i++) {
    let a = i;
    console.log(a);
}

// 1 2 3 4 5

仮に1つのfor文における全ループブロックが単一のスコープであれば
let aでの再宣言でエラーが発生しているはずですが、
エラーは発生していないのでそういうことなのです。

この辺りまでは調べればそこそこ出てきます。

巻き上げ的な挙動はあるが、(巻き上げが問題になるようなコードであれば)エラーになる

console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
let a;

MDN

undefined の値で始まる var 変数と異なり、 let 変数は定義が評価されるまで初期化されません。変数を宣言より前で参照することは ReferenceError を引き起こします。ブロックの始めから変数宣言が実行されるまで、変数は "temporal dead zone" の中にいるのです。

定義を評価というのが具体的に何を指すのかちょっと曖昧な気もしますが

console.log(a);

// ひとつ上のコードとはちょっと違い、変数が存在しないエラー
// Uncaught ReferenceError: a is not defined
let a;
console.log(a);

// undefined

という挙動から考えると、let宣言された行に到達=定義を評価と考えて良さそうです。
宣言自体は巻き上がっておらず、ブロック開始時点で宣言前の準備が行われている、という感じですね。

いずれにしても、このエラーが発生するようなコードには構造上の問題があると考えた方が良いと思います。

これを知ってたら中級くらい? 実務上で遭遇することは結構ありそう。

あんまり知られてなさそうな性質

本題です。
冒頭にも書きましたが、以下はfor文との組み合わせの際に表れる性質です。

for文の存在するブロックスコープ」と「forループブロックスコープ」の間に、見えないブロックスコープが存在する(ような挙動をする)

{
    // スコープ①
    for (let i = 0; i < 10; i++) {
        // スコープ②
        i++;
        console.log(i);
    }
    console.log(i);
}

// Uncaught ReferenceError: Cannot access 'i' before initialization

エラーが出ていることから、for文の初期化部はスコープ①ではないことが分かります。

仮にスコープ②であったとしたら、各ループブロック内でのlet i宣言はブロックごとに個別の変数になるので、
i++でループ数が減少する説明がつきません。

なのでイメージとしては

{
    {
        let i;
        for (i = 0; i < 10; i++) {
            i++;
            console.log(i);
        }
    }
    console.log(i);
}

のようなコードの挙動が近しいです。

※このコードはあくまでイメージです。
 後述する別の性質を破壊してしまうので、使ってはいけません。

for文において、ある条件下でのみ「別の変数の参照(のような挙動)」が起こる

発端となった挙動の原因です。

なぜこうなるのか

それらしい記載のあるサイトがあまりなかったのですが

つまり、for (let...)ループが複数回実行され、そのループにクロージャーが含まれている場合(話している猫の例のように)、すべてのクロージャーが同じループ変数をキャプチャするのではなく、各クロージャーがループ変数の異なるコピーをキャプチャします。

とのこと。

発生する条件は異なりますが、関数内でargumentsという変数が自動で使えるようになっていたり、
状況に応じたオブジェクトがthisに束縛されているのに近しいイメージでしょうか。

確かに

for (let i = 0; i < 5; i++) {
    setTimeout(() => {
        i *= 2;
        console.log(i);
    }, 100);
}

// 0 2 4 6 8

と、各クロージャ内でのiは、それぞれ別のiとして存在しているように見えます。

これなら確かにBの挙動になるのも頷けるのですが・・・

色々試してみる

色々なコードを試してみるうちに、この説明もちょっと違っている気がしてきました。
(5年前の記事なので、当時とパーサの解釈が違う可能性はありますが)

即時関数

for (let i = 0; i < 5; i++) {
    (() => {
        i++;
        console.log(i);
    })();
}
console.log('end');

// 1 3 5 end

クロージャ内の処理ではあるものの、普通にfor文の初期化部のlet iを参照しちゃっているようです。

関数定義+同期実行

for (let i = 0; i < 5; i++) {
    function f() {
        i++;
        console.log(i);
    };
    f();
}
console.log('end');

// 1 3 5 end

やってることは即時関数と変わらないので、まあこうなるよなといった印象。

Promiseexecutor

for (let i = 0; i < 5; i++) {
    new Promise(resolve => {
        i++;
        console.log(i);
    });
}
console.log('end');

// 1 3 5 end

Promiseと言えどexecutorは同期的に実行されるので、まあこうなるよなといった印象。

Promise.then

for (let i = 0; i < 5; i++) {
    new Promise(resolve => {
        resolve();
    })
    .then(() => {
        i++;
        console.log(i);
    });
}
console.log('end');

// end 1 2 3 4 5

動いた。
ほぼ即時的な処理であっても非同期処理であればいいんだろうか?

setTimeoutdelay = 0

for (let i = 0; i < 5; i++) {
    setTimeout(()=>{
        i++;
        console.log(i);
    }, 0);
}
console.log('end');

// end 1 2 3 4 5

こっちもPromise.thenと同じような処理だしやはり動いた。

setInterval

for (let i = 0; i < 5; i++) {
    let c = 0;
    const p = setInterval(()=>{
        i += 10;
        console.log(i);
        if (c++ === 1) {
            clearInterval(p);
        }
    }, 0);
}
console.log('end');

// end 10 11 12 13 14 20 21 22 23 24

同一の関数を複数回実行しても、ちゃんとそれぞれのクロージャごとに個別のiを参照していそう

ここまでくると、クロージャが非同期処理として実行されることが条件のようにも思えますが・・・

クロージャをfor文の全ループの完了後に実行

const a = [];
for (let i = 0; i < 5; i++) {
    a.push(function() {
        i++;
        console.log(i);
    });
}
console.log('end');
for (let j = 0; j < a.length; j++) {
    a[j]();
}

// end 1 2 3 4 5

非同期処理ではないですが、同期処理としての順番を変えたら参照する変数が変わっているようです。

念のため、外部スコープの配列に格納しているせい、という可能性もあるので・・・

const a = [];
for (let i = 0; i < 5; i++) {
    a.push(function() {
        i++;
        console.log(i);
    });
    a[a.length - 1]();
}
console.log('end');

// 1 3 5 end

let i参照に戻っているので、特に関係はなさそうです。

新たな参照先が生成されるタイミング

クロージャがそれぞれ個別のiを生成するタイミングも調べておきます。

for (let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, 10);
    i++;
}

// 1 3 5

ちょっとわかりづらいですが、

  1. let i = 0 (0)
  2. クロージャ定義 (0)
  3. ループ内最下部のi++ (1)
  4. for文の更新部のi++ (2)
  5. クロージャ定義 (2)
  6. ループ内最下部のi++ (3)
  7. for文の更新部のi++ (4)
  8. クロージャ定義 (4)
  9. ループ内最下部のi++ (5)
  10. for文の更新部のi++ (6)
  11. for文終了 (6)

()内は そのときのiの値です。

どうやら、クロージャが定義された瞬間のiではなく、
クロージャ定義後のi++が反映された状態のiが新たな参照先変数の値になっているようです。

結論

ということで、Bのような動作をする理由は、

1. for文の初期化部で変数がlet宣言されている(仮にlet iとする)
2. forループブロック内で関数(クロージャ)が定義されている (仮にfとする)
3. fの内部から、iが参照されている
4. forループ処理が完了した後にfが実行されている

の全ての条件を満たした場合

f内で参照しているiの値が、「f実行時のiの値」ではなく「fが定義されたスコープの末端に到達した時点でのiの値」に束縛される

という性質に当てはまっているコードだから、ということなのではないかと思います。

ちなみに条件の1についてですが、letでの宣言をfor文の初期化部以外にしてしまうと

let i;
for (i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, 10);
}

// 5 5 5 5 5 

と、クロージャごとに個別の参照先にならなくなります。
あくまで for文の初期化部でlet宣言した時のみ、の性質ですね。

起こりえる問題

for (let i = 0, finished = 0; i < 5; i++) {
    setTimeout(() => {
        if (++finished === 5) {
            console.log('finished');
        }
    }, Math.random() * 100);
}

for文の初期化部は独立したスコープになっている、という性質を考えると
forループ内からのみアクセスできる変数が欲しいみたいなときに、
for文の初期化部に宣言しちゃうことがあるかもしれません。

上のコードでは、finishedを「完了した非同期処理の数を表すカウンタ」のつもりで定義しています。
ですが、この性質のせいでfinishedもクロージャごとに個別な存在になってしまい、let finishedはいつまで経っても5になりません。

初期化部で複数の変数を宣言するコードは割と見かけるので、意外とハマる可能性はあるかも?

余談

ずっと for文で説明してきましたが、for-infor-of でも同様の挙動になります。

また for-infor-ofでは、letの代わりにconstを使用することができますが、
この場合もやはり同様の挙動になるようです。

まとめ

なんだかletの性質というより、for文の性質になってるような気がしつつ・・・。
javascriptにもまだまだ知らないこと沢山だなー。

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

色空間を 3D で可視化した Web アプリで色への理解を深める

0. はじめに

色に関して「ちゃんと知りたい・理解したい」と思っている方、実は多かったりしないでしょうか。
私は以前、下記のようなもやもやした気持ちを抱いていました。

  • 色が見える根本的な仕組みを知りたいけど、なんか難しそう
  • 「RGB」とかって、なんとなく分かるけどよく知らない
  • 「彩度」と「明度」という言葉をたまに聞くけど、ぶっちゃけよく分からない(でも日常生活は困らない)
  • 資料作成やデザイン関連作業で、色をどうやって選んだらいいか分からない・なかなか出したい色にたどり着けずイライラする

本記事では、上記のような色に関する素朴な疑問の解決(もしくは解決への糸口)を可能な限り目指して、色及び色空間に関する基礎的な内容をご紹介したいと思います。

背景としては、この年末年始に下記動画のような 色空間を 3D で可視化する Web アプリケーション ―ColorSpace― を趣味で開発したこともあり、開発の目的だった「色空間の直感的な理解」を一人でも多くの人にしていただけたら嬉しい、という想いがあります。
320_ezgif-6-4a21c655e924.gif

本記事はまず基礎的な内容として、「1. 色が見える仕組み」に関してざっくりふれ、色の表現方法である「2. 表色系と色空間」に関する解説をし、web やソフトウェアの世界で一般的と思われる RGB と HSV 色空間に関して説明いたします。最後に「3. アプリケーションの説明」にて今回開発したものの背景・概要を解説いたします。

「1. 色が見える仕組み」「2. 表色系と色空間」は特に基礎的な内容であるため、画像処理に関わっている方など色に十分詳しい方はターゲットとしてはいませんが、「3. アプリケーションの説明」はもしかしたらお楽しみ頂けるかもしれません。
また、「色選びが難しい理由」に関してはちょっとだけ触れますが、配色と呼ばれる色選び自体に関しては本記事では触れません…配色関連の書籍は数多くあるので、それらを参照していただければと思います。

Web アプリ閲覧にあたっての注意事項
Web アプリ閲覧にあたってはちょっとした注意事項があります
①スマホ・タブレットでもキレイに表示されるよう実装していますが、
 GPU をゴリゴリ使うために消費電力が大きく、外出中はバッテリーの持ち時間が危うくなるかもしれません。
 出先では少し覗くに留まり、もし気に入っていただけたら、お家に帰った後に
 お茶を飲みながらスマホ充電しつつ見るか、電源に接続された PC 等で
 ゆっくり眺めて頂ければと思います…
②最近のスマホ・PC・タブレット等の端末であれば、ほぼ問題なくぬるぬる滑らかに動作しますが、
 端末の GPU の性能によっては、カクカク動いたりレスポンスが悪くて宇宙空間を漂うような
 無重力感(というか無力感)を感じることもあります。すみません。

上記2つの問題への対応として、GPU の負荷が小さくなる LITE バージョンも実装しました。
あくまで推奨は通常版ですが、あまりにもレスポンスが悪い場合には LITE バージョンを試していただければ少しはマシになるかもしれません。

1. 色が見える仕組み

我々が色を見ることができるのは、物体に反射している光を目がキャッチし感知しているためです。光がない暗闇では我々は色を感じ取れません。つまり、目に届く光によって我々は物の色を知覚します。

光は波で出来ており1、我々が普段見ている太陽光や照明光は無数の様々な波長の光を含んでいます。その中で、我々が明るさや色を感じることが出来る波長の光は可視光線 (Visible Light) と呼ばれます。可視光線は波長の範囲が大体決まっていて、その範囲を外れた波長のものは我々人間の目には見えず、明るさも色も感じられません。そこいら中に飛んでいるはずの電波が全く目に見えないのと同様(光は電波と同じく電磁波の一種です)、太陽光の中に含まれる赤外線や紫外線も我々には見ることが出来ません2

可視光線を、連続した波長ごとの成分に分解させたものをスペクトル3と呼び、虹やスマホに反射した光など、日常生活でも見ることができます4。可視光線がカラフルに分解されて見えるこの現象は、光は波長の違いによって別々の色に見える、ということを示しており、「可視光線上のそれぞれの波長の光」は特定の色に紐付いている、ということが分かります。例えば 400nm の波長の光は紫色、600nm の波長の光はオレンジ色、といった具合です。下の図は、スペクトルとして現れる色を波長と共に示した概略図で、波長と色のひも付きを示しています。図上の右端はおおよそ 760nm の波長の光で、この波長の光は濃い赤色に見えるということが読み取れます5。一つだけの波長からなる光を単波長光と言いますが、単波長光は特定の色に決まることから単色光 (monochromatic light) とも呼びます。
201905231723532496272.png
(出典:"LED Wavelength Vs. LED Color Temperature" from Fireflier Lighting Web site)

スペクトル上に見ることのできる単波長光だけが我々の見える全ての色をカバーするわけではありません。例えば、白をはじめとしてピンクや深い緑色などは上図の通り、スペクトル上には見えません。しかしそれらの色も、スペクトル上のたった 3 つの色だけで全てを作り出せるのではないか、とトーマス・ヤングが仮説を唱えました。

今現在ではそれがほぼ事実であることが分かっています。人間の視細胞には三種類あってそれぞれ異なる波長の光刺激に強く反応し、それらの刺激の混ぜ合わせによって見える色が決まる、という仕組みが明らかになっています。そしてヤングが唱えたように 3 つの単波長光でほぼ全ての色を表現できることも分かっています6。それら 3 つの単波長光の色が「光の三原色 (three primary colors) 」と言われる赤・緑・青( R/G/B )であり、三原色だけで我々の見えるほぼ全ての色は再現可能なのです7

なお、様々な物がそれぞれ異なる色に見えるのは、物体によって、どの波長の成分を反射しやすくどの成分を吸収しやすいのか、という特性が異なっているためです。例えば、白い物体は可視光線の中のほとんどの波長の光を反射し、黒いものは可視光線をほぼ全て吸収する8、赤い物体は赤に近い波長のものだけをよく反射する、といった感じです。

以上が、我々が色を見ている仕組みです。

まとめ

  • 人間は光を感知することで色が見える
  • 光は無数の波長の成分からなり、目に見える範囲の光を可視光線という
  • 可視光線の中のそれぞれの波長の光は特定の色と紐づく
  • 全ての色は 3 つの波長成分だけからほぼ再現できる
  • それらの 3 つは三原色と呼ばれる赤・緑・青である

2. 表色系と色空間

表色系とはなにか

前章の通り、人間に見える色は、赤・緑・青の三色の強さの混ぜ合わせによってほぼ表現可能なことが示されていますが、その事実を元に「それぞれの色をどのように表すか」ということを定めたルールを表色系 ( color system ) と言います。読んで字の如くです。ちょっと固く言うと、表色系とは任意の色をどのような形式で定量的に表すか、という体系のことです。

表色系には大きく分けて混色系顕色系の二種類があります。

1. 混色系 (color mixing system)

赤・緑・青の三原色といった知覚の仕組みを元にした成分を定めて、それらの混ぜ合わせにより定量的に表す表色系です。RGB 表色系の他、XYZ 表色系、CMYK 表色系などがあります。

混色系は色の見える原理に根ざした体系であるために、カメラやディスプレイなど色を再現する機器で扱うための形式に適しています。カメラでの色の入力は、人間の視覚を再現したような形で R/G/B のそれぞれの光の強さを記録するように作られていますし、カラーテレビやカラーディスプレイでの出力では、ディスプレイ上の各ピクセルを R/G/B の光の強さを合成することにより、出力すべき色を再現して表示します9

2.顕色系 (color appearance system)

「明るさ」「色合い」「鮮やかさ」といった、見た目の感覚として表現されうる属性を軸として、それらの属性値の組み合わせで表す表色系です。「色の三属性」と呼ばれる「色相」「明度」「彩度」の三属性で各色を表すマンセル表色系が代表的です。

顕色系は、人間が色を選んだり比較したりする際に有用です。別名カラーオーダーシステムと呼ばれるように、色を人間が自然に感じられるように順序良く並べた (order した) もので、色を探しやすく選びやすいように作られています。例えば、同じ赤系列でちょっとくすんだ色を表したい、といった場合や、「この赤と同じくらいの明るさ・鮮やかさの青系統の色が欲しい」といった際に数値としてわかりやすい表現となります。

色空間とはなにか

色空間 (color space) というのは、表色系で定められたルールによって導き出される全て要素からなる色の表現範囲のようなものです10。「RGB 色空間」といえば RGB 表色系によって具現化される全ての要素を含む集合です。
つまり表色系と色空間は、ほぼ同様の概念であると言えますが11、ソフトウェアなど工学関連分野では表色系よりも色空間という言葉がよく使われる気がします12

以下では、色空間としてソフトウェア関連では最も身近な RGB 色空間と HSV 色空間の 2 つを取り上げて説明いたします。

なお、表色系や色空間周りでは、「CIE-RGB 色空間」など CIE が頭につく語が多く登場しますが、CIE は国際照明委員会 (International Commission on Illumination13) のことで、現在広く用いられている RGB 表色系と XYZ 表色系を定めた標準化団体です。CIE が頭に付いているものは CIE が標準化したもの、という意味なので、ざっくりした説明のみの本記事では CIE 系であるか否かは特には触れません。

RGB 色空間

色の三原色である R/G/B の三種類の値の組み合わせで色を表現します。全てが 0 のときは光がない状態を意味して黒を表し、全てが最大値のときには白となります14

RGB_252px-Synthese+.svg.png
(出典:"File:Synthese+.svg" from Wikimedia Commons)

RGB 色空間は、人間の知覚に基づいた定量化方法であるために色を再現するための機器で扱いやすいという混色系のメリットが最大の長所で、画像処理などで画像データを扱う際は RGB のまま扱うことが多いです。

しかし、人間には R/G/B の値を色として直感的に捉えることが難しい、というデメリットがあります。普段我々の生活の中である色を見たときに、赤・緑・青の刺激が混ぜ合わさった状態のものを脳が認識しており、分割して感じているわけではないためです。例えば、鮮やかなピンク色を再現するためには、赤・緑・青をどれくらいの強さで混ぜ合わせたらいいのか、をすぐに思いつける人は少ないのではないでしょうか。また、現在見ている色を、色合いを変えずに明るくしたい、といった場合に、R/G/B のそれぞれの値をどのように変化させたらいいかも、直感的には分かりづらいかと思います15

資料作成などで色を選ぶ際、下記のような R/G/B のそれぞれの値を変化させるものを触ったことのある人も多いかと思いますが「なんだか扱いにくい」と感じるのは、仕組み上自然なことだと思います。

color_selection_rgb_bars.gif

HSV 色空間

HSV 色空間に代表される顕色系は、RGB 色空間等の混色系の問題点である「数値の直感的な理解が難しい」という問題を解決します。

HSV 色空間は、「色の三属性」と呼ばれる、見た目の感覚として表現可能な属性を軸とする顕色系の代表選手、マンセル表色系に近い表現方法です。色相 (Hue)、彩度 (Saturation)、明度 (Value または Brightness) の三属性の値の組み合わせによって表現します。

320px-HSV_color_solid_cone.png
(出典:"HSV color solid cone.png" from Wikimedia Commons)

色相は、「赤系統」や「青系統」といった色味で、色相環と呼ばれる色合いを円状に並べて表記されることが多いです。
彩度と明度は分かりにくいのですが、「彩度は色の鮮やかさ」「明度は明るさ(そのままですが)」と考えると分かりやすいです。彩度が高いものはビビッドカラーと言われる原色に近い色で、彩度が 0 のものは、モノトーンと呼ばれる白・黒・グレーです。つまりビビッドな色から彩度を落とすと、段々とモノトーンの色に近づいていきます。重要なポイントは、「同じくらいの明るさに感じるものでも、色の鮮やかさが異なるものがある」という点で、下の図を見ていただくと、明るさをほぼ保ったまま、色を鮮やかなものからくすんだ色まで変化させることが可能なことがわかるかと思います。

sat_02.png
sat_01.png

厄介な点は、彩度と明度は単純に切り分けることが出来ず、明度を落としたときに彩度の取りうる幅も小さくなる、という点です。例えば黒は、彩度が 0 のものしか存在し得ません。下図において、下の方にいくと横幅いっぱいに黒くなるのはそのためで、立体として表す場合には円柱でなく円錐で表現されることが多いのも同様の理由です。独立していて欲しいはずの属性間に関連がある、という点が、この色空間の難点であるように個人的には思います。

color_selection_hsv_rectangle.gif

なお、HSV (Hue/Saturation/Value) と HSB (Hue/Saturation/Brightness) は同じものを指します。HSL (Hue/Saturation/Lightness or Luminance) と HLS (Hue/Lightness or Luminance/Saturation) も同じものを指します。HSV と HSL は似ていますが若干異なります。紛らわしいですね… さらに紛らわしいことを言うと HSI (Hue/Saturation/Intensity) は HSL/HLS と HSV/HSB を全て含めたことを指します。HSI は色空間の理論の説明では出てきますが実用上にはあまり登場しないため、個人的には HSV/HSB と HSL/HLS は違うということだけ押さえておけばよいかと思っています。

まとめ

  • 表色系とは色を表す体系のこと
  • 色空間とは色を表した時の表現方法とそれが包括する範囲のこと
  • RGB 色空間とは R/G/B の強さで色を表す、混色系の代表方式
    • 原理に根ざしたため便利であるが、人間には分かりづらい
  • HSV 色空間とは 色相/彩度/明度で色を表す、顕色系の代表方式
    • 感覚を元にしているため分かりやすいが、明度と彩度などは若干理解しにくい
    • 内部では R/G/B 等に変換して使用される

3. アプリケーションの説明

開発の背景

今まで見てきたように色を定量的に表す際、RGB 色空間では赤と緑と青の強さ、HSV 色空間では色相と彩度と明度という 3 つの数値を用います。その他にもここでは挙げていない様々な色空間がありますが、ほとんどの場合、一つの色は 3 つの数値の組み合わせ(三次元ベクトル)として表現されます16。ここで大事なのは 「色は基本的には一次元もしくは二次元では表現しきれない」 という事実です。つまり、「一本の数直線」(一次元)や「座標平面」(二次元)で表現することは本質的に不可能だということです。

前章でも若干触れましたが、資料の作成やデザイン関連の作業で色を選ぶ際、なんとなく難しさや違和感を感じる方も多いのではないかと思います。RGB のバーで探しにくいのは上述の通り自然なことと思われますが、HSV 形式のカラーピッカーも私はなんだか探しにくい、と感じてきました。その原因は、本来三次元である色空間を、無理やり一次元のバーと二次元の平面で表現しているために、空間全体を俯瞰することができず規則性も直感的に把握することが難しいためではないかと考えています。

そのため、「三次元空間上に色を秩序正しくキレイに並べてみたら、色全体を俯瞰できていい感じになるのでは…」、と思い試してみたのが今回開発したアプリケーションです。
少し調べてみても同様のことをやっている人はほとんどいなかったため17、実際に自分で見てみたく Three.js を初めて触りながら作ってみました18

色空間に対する直感的な理解が深まるような可視化を目指していたので、このアプリケーションを通じて色に対する理解が深まる方が少しでもいれば幸いです。

あとは、実際に開発したものをソースコードと共に公開したら、転職活動の際に話のネタになるかなーと思ったり…

詳細

今回開発したアプリケーションは、色空間を 3DCG として可視化したものです。3DCG のインタラクティブな操作を通じて、人間が知覚可能な色の範囲を俯瞰しつつ、色と色の関係性や混ざり具合などを具体的に把握することを目指しています。

前章の RGB、HSV 色空間の色の並びを可視化し、様々な角度・距離で見てみたり、所望の色の RGB 値を確認したりできます。
320_ezgif-6-4a21c655e924.gif

特に個人的におすすめな点は、RGB, HSV の任意の軸の値で絞り込んだものを表示できる、という点です。例えば、RGB の赤の強い値を固定した時に G/B の値の変化でどのように色が変化するか、というのを軸を絞り込んで見たことができたり、HSV の最も明度が高い分類となる色の群は、RGB 色空間では立体の外側に来る、ということを確認したりできます。
320_ezgif-6-727f6da8a2b5.gif 320_ezgif-6-79e0a447ce79.gif

具体的な操作方法は、ドラッグやスクロール、ピンチイン / ピンチアウトなどを試していただければ直感的に把握できるようにしてみているので、詳細な説明は省略いたします19
一つだけ中々発見できないかもしれない操作として「視点の平行移動」があり、「右クリック + ドラッグ」「Ctrl + ドラッグ」「二本指でドラッグ」のいずれかで可能な操作です。

また、実装上の技術的な詳細等に関しては本記事の趣旨ではないので割愛いたしますが、もし需要があれば記事を書きたいと思います。ソースコードは全てこちらの GitHub レポジトリで公開しています20

アプリケーションに関する想定問答

  • RGB と HSV を切り替えた時、対応しているはずのキューブで若干 RGB の値が異なっている?
    • RGB と HSV の両表現とも「なるべく網羅的に色を表示する」「位置関係に応じた色の配置にしている」ため、対応キューブの色が若干異なってしまっています。すみません。
  • 全部の色は表現できてなくない?
    • はい、現在表示しているのは代表的と算出した 512 色のみです。「全部の色の数」は、色の表現形式によりますが、近年最も一般的に使われる、R/G/B 各チャンネルで 8bit (256 階調) ずつデータを保持する形式だと $256^3$ で 16,777,216 色(!)あります。アニメーションなどを使用して、「一つのキューブがクリックされた際にその近辺の色を表示しドリルダウンしていく」等の手法によって全ての色を探せるようにする方法は考えられますが、需要が謎なのでそこまでやってみていません。もしその機能も欲しい方がいたら実装するかもしれませんので、ご要望有りましたら GitHub の issue に書いて頂くか本記事のコメントにご記載下さい。
  • 何の役に立つの?
    • 多分ほとんど役に立つことはないです。すみません。
    • 当初はデザイナーさんとかが色選びの時に使えるようなツールに出来たら嬉しかったのですが、おそらくそのようなレベルには程遠いのではないかと思います(そもそもデザイナーさんの色選びの作業プロセスを知らない…)
    • さらに、美しく見えることも重視していて光や影の表示をいい感じになるように調整しているので21、「厳密に色を選びたい」という想いには応えられそうにありません。
    • もしこのアプリケーションを通じて色空間に対する直感的な理解が深まる方が 1 人でもいるならば、開発者としてはもうそれで十分満足です。
    • 個人的には、このアプリケーションで三次元空間上の浮遊物体を眺めつつグリグリ動かしたり、キューブをクリックしてくるくる回してみたり、色々な軸の値で絞り込んでみたり、キューブ群の浮遊しているど真ん中に突撃しにいってそこからの様々な眺めを愛でてみたりするだけで、十分気持ちよかったり楽しかったりします。

4. おわりに

色というのは、日常的なものでありながらも直感的に把握しづらい事実があったり、とても奥深くとても面白いものです。
本記事の目的通り、これをきっかけに少しでも色に対しての理解が深まったり今まで以上の興味が湧いたり、色の世界は面白い・キレイだ、などと感じて頂ける方がいれば幸いです。

上でも書いたとおり、今回私が開発したものはほとんど実際の役に立つものではないと思います。ただ、今回の開発では、色を網羅的にかつ秩序正しく並ばせてみただけで、こんなにも美しいものが現れた、という事実を知れて私はとても嬉しかったです。色覚は個人差も大きく、皆が皆同じように見えているわけではないという事実はありながらも、我々の過ごす色彩に溢れているこの世界は、本当に美しく素晴らしいものだと改めて思いました。

駄文ながら最後までお付き合い頂きありがとうございました。

参考文献・参考サイト

書籍

色彩工学入門 -定量的な色の理解と活用- (ほげほげ著)

研究者の方が書かれた色に関する科学に関しての学術書です。とても深い内容でありながらわかりやすく書かれています。色彩に関してしっかりと勉強したい、深く知りたい、詳しくなりたいという方向けです。

ディジタル画像処理[改訂新版]

画像処理全般に関して、かなりの内容を網羅している書籍です。その上、浅かったりいい加減な記述はほぼありません。色の理論・扱いは、画像処理では必須なこともあり、第3章「画像の性質と色空間」にほどよくまとめられています。この書籍は画像処理関連に携わるエンジニアの方の多くが一度は目を通しているのではないかと思いますし、画像処理に興味のある方にはオススメの一冊です。

色彩検定 公式テキスト 3級編

色彩検定という資格試験のテキストです。学術的な難しい話はなく、数式等が苦手な方でもとても読みやすいながら色に対しての理解は深まる一冊だと思います。ちなみに私は色彩検定の資格は毎年取りたいと思いつつ、受けそびれてしまっています…

参考サイト

three.js

Three.js の公式サイトです。やはり開発時に最も頼りになるのは公式ドキュメントです。

Three.js 入門サイト (ICS media 内)

Three.js に関する初歩的な内容がとてもわかりやすくまとまっています。大変お世話になりました。

Three.jsのパフォーマンスTips (Kabuku 社開発者ブログ)

Three.js での負荷軽減、パフォーマンス最適化に関して、Three.js 作者自らのツイートを解説付きで紹介しています。こちらのサイトもお世話になりました。


  1. 厳密ではありません。実際は波でありかつ粒でもあります。このような、全く直感的でない光の性質は実験によって証明されています。気になる方は量子力学関連の書籍を参照して下さい。 

  2. 紫外線が見える人も一部いるみたいです。なんということでしょう。 

  3. 「スペクトル」という語は、光のスペクトル以外でも使われるため本文脈でのスペクトルは正確には分光スペクトルと言います。例えば音を周波数成分ごとに分解したものなどもスペクトルであり、周波数スペクトルなどと呼ばれます。 

  4. 虹のスペクトルは、空気中のしずくにより光が若干複雑に分散されたものです。 

  5. この状態で両端に現れるのは、可視光線の中で最も波長が長い赤のものと、最も波長が短い紫のものです。赤外線、紫外線は、これらの波長の外にあることからそう呼ばれます。 

  6. 実際は 3 つの単波長光の混ぜ合わせだけでは全ての色は再現できません。3 つの視細胞が感知する波長の範囲には重なりがあることにより、一部の領域において赤の成分を負の値にしなければ表現できない色が存在します。しかし、線形代数のちょっとした計算で基底を変えることにより、全ての色を基底の正の値のみで表現できることが可能となります。それを色空間としたものが XYZ 色空間であり、XYZ 色空間では全ての色を表現可能であるため、色の値の情報伝達手段として内部的に扱われることが多いです。ただし、基底となる波長は実際に目に見えない色となってしまうこともあり、我々の目に触れる範囲では RGB が使われることが多いです。 

  7. 逆に言えば、人間は「赤」「緑」「青」と人間が名付けた色の波長しか感知できないために、それらの組み合わせで作られる色だけしか見えない、ということです。他の生物では 4 種類の視細胞で色を感知する動物もおり、例えばモンシロチョウの羽を紫外線センサーを使って見ると、人間には見えない模様が見え、モンシロチョウにはその模様で雄雌の区別ができるという話もあります。しかし、それらの動物に見えている 4 種類の原色が混ざりあった色の景色はどうやっても人間には想像できません。 

  8. 黒いものが光に当たると温まりやすいのは、光を吸収する割合が他の色のものと比べて大きいためです。 

  9. これらの機器は「どのようにしたら人間が同じと感じるように色を記録・伝達・再現することができるか」という目的で作られているため、人間の知覚を再現するような仕組みであるのは当然のことなのですが、それは人間だけを対象とした仕組みであることは興味深い事実です。すなわち、実物とそれを撮影した写真や映像は人間にはほぼ同じものとして見えていても、紫外線が見えるような人間と知覚特性の異なる動物には全く別の色合いに見えている可能性があります。 

  10. 厳密な定義はとても難しい用語なので、間違いではないわかりやすい表現を頑張って捻出しています。表色系と違う点は、色空間は数学的な意味での代数構造としての空間、つまり色を多次元ベクトルとして捉えた場合の N 次元のベクトル空間のニュアンスが強い点だと思っています(が、私も専門ではないので確かではありません…)。 

  11. 実際、英語では表色系を表す color system と色空間を表す color space は同義として扱われています。 

  12. 理由として(完全に私見ですが)、工学系では色をベクトルとして扱うことが多く線形変換など数学的な処理を頻繁に行うために、代数系としての空間という語が馴染みやすいからではないかと思っています。 

  13. 元はフランス語で "Commission internationale de l'éclairage" としていたため CIE という略語となっています。 

  14. 厳密には、白に関しても「どういった色を白とするか」を決める必要があります。 

  15. 訓練すればどれくらいの比率で R/G/B を混ぜれば任意の色を作れるかわかるようになるのかもしれません。例えば料理人は「酸味をもうちょっと足したほうが味のバランスがよくなりそう」という想像ができるようになるのと同じで、「こういう色が欲しい場合には青と緑を 2:5 の割合くらいで足せばよい」となるかもしれません…そもそも画家さんは自分で色を混ぜて好きな色を作れてそうですし… 

  16. 「基本的に」と書いたのは、透過チャネルを加えた四次元で表現されることも多いためです。 

  17. 今回じっくり調べてみたら、既に類似のものがありました…見つけてしまってちょっぴり凹みましたが、以前に見つけていたら今回自分は作らなかったかもしれず、逆に今まで発見していなくてよかった、という気持ちにもなりました。 

  18. 実は最初は、某企業に応募した際に出された課題の中の「自由課題」を選択して開発し、今回作り直した感じです。 

  19. 説明書的なドキュメントを作るのが面倒というのもありますが、アプリ自体が取り扱い方を説明してくれるようなソフトウェア(いわゆるアフォーダンスに溢れているようなもの)を作れたらいいなーと個人的には常に思っています。自分の作ったアプリで「これは何が表示されているのですか?」とよく質問され、その度に「まだまだだなー」と思う毎日です… 

  20. Three.js はガチ初心者なので実装として拙い部分は多々あるかと思います。効率の良い書き方・一般的な実装方法などご指摘ありましたら頂けるととても嬉しいです。 

  21. 光や影の表示に無駄にこだわっているために、一部端末で激重だったり消費電力が激しかったりします… 

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

webサイトの更新の表示をgithubのreleaseで管理

TD;LR

webサイトの更新履歴をgithubのリリースで管理したら便利だった
releaseの更新に合わせてツイートをするgithub actionsを作った

releaseで管理

よくウェブページについている下みたいな更新履歴を作ろうと思ったけど

20xx/0x/xx xxxxxの内容を更新
20xx/0x/xx xxxxxを追加
20xx/0x/xx xxxxxを更新

ファイルにまとめたりして一々追記していくのが嫌だったのでgithubの機能を使いました。

release機能

releaseはgithubの機能でリリースノートや添付ファイルを付けてユーザーに公開する事ができます。
image.png
ここのreleasesと書いてある部分から作成でき、
releaselog.PNG
今までの履歴がこのように表示されます。

こんな感じでピッタリな機能だったので今回の更新履歴の実装に使ってみることにしました。

releaseの取得

releaseの取得にはgithubのrest APIクライアントのoctokit/restを使用しました。

npm install @octokit/rest

上のnpmコマンドでパッケージのインストールをしたあとはコードを書いていくだけです。

import Octokit from "@octokit/rest";

let releaseLog = []
const octokit = new Octokit();

取得したreleaseを入れておくリストとOctokitの宣言を済ませたら。

octokit.repos.listReleases({
  owner: "mugi111",
  repo: "my-profile-page",
}).then((res) => {
  if (res.status === 200) {
  // 整形
  }
})

ownerでユーザー名repoでリポジトリ名を指定してlistReleaseでreleaseの一覧を取得します。
一覧が返ってきたら自分の使いやすいように整形して最初に宣言したreleaseLogに入れておきます。

あとはループさせたりして表示させるだけです。

おわり

更新履歴に追加したいところでrelease作成すれば自動的にページに反映されるようになるので楽になった。
githubからの取得も意外と簡単だった。
github actionsについては次のでちゃんと書きます。

更新履歴を実装したページとリポジトリ
https://th-mg-profile.netlify.com/
https://github.com/mugi111/my-profile-page

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