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

PA-API v5で商品情報を取得する

PA-APIの利用

PA-API を使ってアマゾン商品情報をとってみよう。そこからアフィリエイトなどで収益化ができたら良いね。

 PA-APIはアマゾンで一ヶ月以内に売上が発生していないと使えません。なので、アマゾンの商品リンク作成ツールや、ブログのアフィリエイトツールなどで売上を得られるように頑張ろう。ここが一番難易度が高い気がする。

 アフィリエイトで売上が発生したら次に、ISBNやASINが必要となる。ただ、ASINを具体的にどのように取るかの選択が少ないので、アマゾンのサイトをクロールしている。

Amazonの検索結果からasinをスクレイピング
https://qiita.com/99nyorituryo/items/c5d53a3ca8a4967b5927

ASINから商品情報のJSONを取得する

 ここでは、ISBNやASINという本の情報から、アマゾンの表紙画像や、タイトル著者名、出版社、価格、本の種類などの情報を取得してサイトとして出力する方法を書く。
 PAAPIでは、様々な言語用にツールが配布されているが、私はnode.jsが一番慣れているので、node.jsを用いた方法について書いていきます。

node.jsのやり方は公式のページから見てみると良いのだけど。

https://affiliate.amazon.co.jp/help/node/topic/GZH32YX29UH5GACM

ここで得られたISBNをsampleGetItemsApi.jsを使ってアマゾンから書誌情報を取得する。
sampleGetItemsApi.jsは下記アドレスのnode.jsからダウンロードできる。
https://webservices.amazon.com/paapi5/documentation/quick-start/using-sdk.html

  • paapi5-nodejs-sdk-and-samplesをダウンロードして展開して
  • さっきのファイルを解凍したフォルダーで、npm install paapi5--nodejs-sdk --saveでインストール
  • アクセスキーやシークレットキーをsampleGetItemsApi.jsなどのサンプルに追加
  • またアフィリエイト用のパートナータグも追加(-22で終わるやつ)
  • sampleGetItemsApi.jsを実行してみよう

というようなことが書かれています。

sampleGetItemsApi.jsを自分の目的に合うように書き換える。

初期状態では、amazon.co.jpになっていないので修正する必要がある。アクセスキーやシークレットキーを入力しないと実行できない。それから一度に取得できるasinは10個までになっている。それと待ち時間を設定しないとすぐエラーを吐くようである。

  • 100個の情報を取得しようとすると、待ち時間を設定して取得している。
  • key.jsonにアクセスキーやシークレットキー、アソシエイトタグにまとめている。
  • dateが開始日でtodateが終了日になっていてまとめて取得できる。
  • asinの配列は下の方法で作っている。

Amazonの検索結果からasinをスクレイピング
https://qiita.com/99nyorituryo/items/c5d53a3ca8a4967b5927


{"accessKey":"アクセスキー","secretKey" :"シークレットキー","PartnerTag":"パートナータグ"}
fs = require('fs');
var json = fs.readFileSync(__dirname + '/key.json', 'utf-8');
key = JSON.parse(json);

function sleep(second) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve()
        }, second * 1000)
    })
}


var ProductAdvertisingAPIv1 = require('./src/index');
var defaultClient = ProductAdvertisingAPIv1.ApiClient.instance;
defaultClient.accessKey =key.accessKey//
defaultClient.secretKey = key.secretKey//
defaultClient.host = 'webservices.amazon.co.jp';
defaultClient.region = 'us-west-2';
var api = new ProductAdvertisingAPIv1.DefaultApi();
var getItemsRequest = new ProductAdvertisingAPIv1.GetItemsRequest();
getItemsRequest['PartnerTag'] =key.PartnerTag//
getItemsRequest['PartnerType'] = 'Associates';


getItemsRequest['Resources'] =[
  "BrowseNodeInfo.BrowseNodes",
  "BrowseNodeInfo.BrowseNodes.Ancestor",
  "BrowseNodeInfo.BrowseNodes.SalesRank",
  "BrowseNodeInfo.WebsiteSalesRank",
  "Images.Primary.Small",
  "Images.Primary.Medium",
  "Images.Primary.Large",
  "ItemInfo.ByLineInfo",
  "ItemInfo.ContentRating",
  "ItemInfo.Classifications",
  "ItemInfo.ExternalIds",
  "ItemInfo.ManufactureInfo",
  "ItemInfo.ProductInfo",
  "ItemInfo.Title",
  "Offers.Listings.Price"];

var callback = function (error, data, response) {
    if (error) {
        console.log('Error calling PA-API 5.0!');
        console.log('Printing Full Error Object:\n' + JSON.stringify(error, null, 1));
        console.log('Status Code: ' + error['status']);
        if (error['response'] !== undefined && error['response']['text'] !== undefined) {
            console.log('Error Object: ' + JSON.stringify(error['response']['text'], null, 1));
        }
    } else {
    //    console.log('API called successfully.');
        var getItemsResponse = ProductAdvertisingAPIv1.GetItemsResponse.constructFromObject(data);
        console.log(JSON.stringify(getItemsResponse, null, 1));
jsondata=jsondata.concat(getItemsResponse.ItemsResult.Items);
        fs.writeFileSync(__dirname + '/json/kindle_paapi/'+filename, JSON.stringify(jsondata, null, 1),'utf-8')
//console.log("test"+jsondata)
        if (getItemsResponse['Errors'] !== undefined) {
            console.log('\nErrors:');
            console.log('Complete Error Response: ' + JSON.stringify(getItemsResponse['Errors'], null, 1));
            console.log('Printing 1st Error:');
            var error_0 = getItemsResponse['Errors'][0];
            console.log('Error Code: ' + error_0['Code']);
            console.log('Error Message: ' + error_0['Message']);
        }
    }
};

(async function(){

date=20200602
todate=20200602
for (date; date <= todate; date++) {
var json = fs.readFileSync(__dirname + '/json/kindle_asin/'+date+'k.json', 'utf-8');
asinarry = JSON.parse(json);

//c=a.ItemsResult.Items.concat(b.ItemsResult.Items);
jsondata=[];
filename=date +'.json'

for (let i = 0; i < asinarry.length; i += 10) {
 await sleep(3) 
asin =asinarry.slice(i, i+10)
getItemsRequest['ItemIds'] = asin


try {
api.getItems(getItemsRequest, callback);
} catch (ex) {
  console.log("Exception: " + ex);
}
}}
})();

これらを利用して作ったのが下のサイトである。

https://kyukyunyorituryo.github.io/new_epub/

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

PA-API v5でAmazonの商品情報を取得する

PA-APIの利用

PA-API を使ってアマゾン商品情報をとってみよう。そこからアフィリエイトなどで収益化ができたら良いね。

 PA-APIはアマゾンで一ヶ月以内に売上が発生していないと使えません。なので、アマゾンの商品リンク作成ツールや、ブログのアフィリエイトツールなどで売上を得られるように頑張ろう。ここが一番難易度が高い気がする。

 アフィリエイトで売上が発生したら次に、ISBNやASINが必要となる。ただ、ASINを具体的にどのように取るかの選択が少ないので、アマゾンのサイトをクロールしている。

Amazonの検索結果からasinをスクレイピング
https://qiita.com/99nyorituryo/items/c5d53a3ca8a4967b5927

ASINから商品情報のJSONを取得する

 ここでは、ISBNやASINという本の情報から、アマゾンの表紙画像や、タイトル著者名、出版社、価格、本の種類などの情報を取得してサイトとして出力する方法を書く。
 PAAPIでは、様々な言語用にツールが配布されているが、私はnode.jsが一番慣れているので、node.jsを用いた方法について書いていきます。

node.jsのやり方は公式のページから見てみると良いのだけど。

https://affiliate.amazon.co.jp/help/node/topic/GZH32YX29UH5GACM

ここで得られたISBNをsampleGetItemsApi.jsを使ってアマゾンから書誌情報を取得する。
sampleGetItemsApi.jsは下記アドレスのnode.jsからダウンロードできる。
https://webservices.amazon.com/paapi5/documentation/quick-start/using-sdk.html

  • paapi5-nodejs-sdk-and-samplesをダウンロードして展開して
  • さっきのファイルを解凍したフォルダーで、npm install paapi5--nodejs-sdk --saveでインストール
  • アクセスキーやシークレットキーをsampleGetItemsApi.jsなどのサンプルに追加
  • またアフィリエイト用のパートナータグも追加(-22で終わるやつ)
  • sampleGetItemsApi.jsを実行してみよう

というようなことが書かれています。

アクセスキーやシークレットキーは下から取得する。
https://affiliate.amazon.co.jp/assoc_credentials/home

sampleGetItemsApi.jsを自分の目的に合うように書き換える。

初期状態では、amazon.co.jpになっていないので修正する必要がある。アクセスキーやシークレットキーを入力しないと実行できない。それから一度に取得できるasinは10個までになっている。それと待ち時間を設定しないとすぐエラーを吐くようである。

  • 100個の情報を取得しようとすると、待ち時間を設定して取得している。
  • key.jsonにアクセスキーやシークレットキー、アソシエイトタグにまとめている。
  • dateが開始日でtodateが終了日になっていてまとめて取得できる。
  • asinの配列は下の方法で作っている。

Amazonの検索結果からasinをスクレイピング
https://qiita.com/99nyorituryo/items/c5d53a3ca8a4967b5927


{"accessKey":"アクセスキー","secretKey" :"シークレットキー","PartnerTag":"パートナータグ"}
fs = require('fs');
var json = fs.readFileSync(__dirname + '/key.json', 'utf-8');
key = JSON.parse(json);

function sleep(second) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve()
        }, second * 1000)
    })
}


var ProductAdvertisingAPIv1 = require('./src/index');
var defaultClient = ProductAdvertisingAPIv1.ApiClient.instance;
defaultClient.accessKey =key.accessKey//
defaultClient.secretKey = key.secretKey//
defaultClient.host = 'webservices.amazon.co.jp';
defaultClient.region = 'us-west-2';
var api = new ProductAdvertisingAPIv1.DefaultApi();
var getItemsRequest = new ProductAdvertisingAPIv1.GetItemsRequest();
getItemsRequest['PartnerTag'] =key.PartnerTag//
getItemsRequest['PartnerType'] = 'Associates';


getItemsRequest['Resources'] =[
  "BrowseNodeInfo.BrowseNodes",
  "BrowseNodeInfo.BrowseNodes.Ancestor",
  "BrowseNodeInfo.BrowseNodes.SalesRank",
  "BrowseNodeInfo.WebsiteSalesRank",
  "Images.Primary.Small",
  "Images.Primary.Medium",
  "Images.Primary.Large",
  "ItemInfo.ByLineInfo",
  "ItemInfo.ContentRating",
  "ItemInfo.Classifications",
  "ItemInfo.ExternalIds",
  "ItemInfo.ManufactureInfo",
  "ItemInfo.ProductInfo",
  "ItemInfo.Title",
  "Offers.Listings.Price"];

var callback = function (error, data, response) {
    if (error) {
        console.log('Error calling PA-API 5.0!');
        console.log('Printing Full Error Object:\n' + JSON.stringify(error, null, 1));
        console.log('Status Code: ' + error['status']);
        if (error['response'] !== undefined && error['response']['text'] !== undefined) {
            console.log('Error Object: ' + JSON.stringify(error['response']['text'], null, 1));
        }
    } else {
    //    console.log('API called successfully.');
        var getItemsResponse = ProductAdvertisingAPIv1.GetItemsResponse.constructFromObject(data);
        console.log(JSON.stringify(getItemsResponse, null, 1));
jsondata=jsondata.concat(getItemsResponse.ItemsResult.Items);
        fs.writeFileSync(__dirname + '/json/kindle_paapi/'+filename, JSON.stringify(jsondata, null, 1),'utf-8')
//console.log("test"+jsondata)
        if (getItemsResponse['Errors'] !== undefined) {
            console.log('\nErrors:');
            console.log('Complete Error Response: ' + JSON.stringify(getItemsResponse['Errors'], null, 1));
            console.log('Printing 1st Error:');
            var error_0 = getItemsResponse['Errors'][0];
            console.log('Error Code: ' + error_0['Code']);
            console.log('Error Message: ' + error_0['Message']);
        }
    }
};

(async function(){

date=20200602
todate=20200602
for (date; date <= todate; date++) {
var json = fs.readFileSync(__dirname + '/json/kindle_asin/'+date+'k.json', 'utf-8');
asinarry = JSON.parse(json);

//c=a.ItemsResult.Items.concat(b.ItemsResult.Items);
jsondata=[];
filename=date +'.json'

for (let i = 0; i < asinarry.length; i += 10) {
 await sleep(3) 
asin =asinarry.slice(i, i+10)
getItemsRequest['ItemIds'] = asin


try {
api.getItems(getItemsRequest, callback);
} catch (ex) {
  console.log("Exception: " + ex);
}
}}
})();

これらを利用して作ったのが下のサイトである。

https://kyukyunyorituryo.github.io/new_epub/

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

Kinx ライブラリ - Getopt

Kinx ライブラリ - Getopt

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。言語はライブラリが命。ということでライブラリの使い方編。

今回は Getopt です。SpecTest で内部的に実装して使っていたのですが、標準ライブラリのほうに移動させました。

ロングオプションにも対応しました(まだリリースしてませんが... リリースしました)。

Getopt - System.getopt

使い方

以下のように、while 文の条件式の場所にオプションの配列とオプション文字列、ロングオプション・オブジェクトを指定する。ロングオプション・オブジェクトは省略可能。

var opt, add, check;
while (opt = System.getopt($$, "a:df", { add: 'a', delete: 'd', help: null, "do-check": '=' })) {
    switch (opt.type) {
    case 'a':               // '--add' でも 'a' が返る。
        add = opt.arg;      // ':' 指定は引数があることを示す。
        System.println('-a with "%{add}"');
        break;
    case 'd':               // '--delete' でも 'd' が返る。
        System.println('-d');
        break;
    case 'f':               // '-f' で返る。
        System.println('-f');
        break;
    case 'help':            // '--help' で返る。
        System.println('--help');
        break;
    case 'do-check':        // '--do-check' で返る。
        check = opt.arg;    // '=' 指定は引数があることを示す。
        System.println('--do-check with "%{check}"');
        break;
    case '-':               // オプションではなかった場合、ここに来る。
        list.push(opt.arg);
        break;
    }
}

// オプション以外の表示
System.println("Program options: ", list);

オプション文字列の詳細

  • 引数有りを指定した場合、引数を指定しなかった時には ArgumentException 例外が送出される。
  • 引数が無いオプションの場合、同じオプション内に次のオプションを指定できる。例えば、-d -f-df と書いても良い。
  • 引数があるオプションの場合、次に文字が続いていれば引数とみなされる。例えば、-a ARG-aARG と書いてよい。
  • 上記 2 つを組み合わせると、-d -a ARG-da ARG とも -daARG とも書くことができる。

ロングオプションの詳細

  • ロングオプションでオプション文字を指定した場合、引数の有無もオプション文字の指定に従う。
  • 引数有りを指定した場合、引数を指定しなかった時には ArgumentException 例外が送出される。
  • ロングオプションの場合は --long-option=argument の形式で引数を指定する。また、ロングオプションの場合は空文字列の引数が許容される。

サンプルの実行

先ほどのサンプルを動作させると次のようになる。

$ ./kinx examples/option.kx -d -a arg
-d
-a with "arg"
Program options: ["examples/option.kx"]

$ ./kinx examples/option.kx -da arg
-d
-a with "arg"
Program options: ["examples/option.kx"]

$ ./kinx examples/option.kx -daarg
-d
-a with "arg"
Program options: ["examples/option.kx"]

$ ./kinx examples/option.kx --help something
--help
Program options: ["examples/option.kx", "something"]

$ ./kinx examples/option.kx --do-check=
--do-check with ""
Program options: ["examples/option.kx"]

$ ./kinx examples/option.kx --do-check=abc
--do-check with "abc"
Program options: ["examples/option.kx"]

$ ./kinx examples/option.kx -a
Uncaught exception: No one catch the exception.
ArgumentException: Needs an argument for -a
Stack Trace Information:
        at <main-block>(examples/option.kx:2)

$ ./kinx examples/option.kx --unknown
Uncaught exception: No one catch the exception.
ArgumentException: Unknown option: --unknown
Stack Trace Information:
        at <main-block>(examples/option.kx:2)

おわりに

オプション解析の方法も色々あって、getopt も歴史が古いですが今でも現役ですね。Most fitting in C programmers の観点では getopt は使いやすいんじゃないかな、と思います。

ヘルプを表示できるという意味では、boost::program_options も捨てがたい。まずは最低限のことができるという意味で System.getopt のサポートです。今後もっと便利なものが出てくるかもしれない(どこから?)。

ではまた、次回。

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

ブックマーク.htmlを初心者でもJavaScriptで整理整頓できるのか

iPhone3G-5sまでアップル漬け。
それ以降Android所有。
Googleを信用せずマイクロソフトを信じてる人。

古いiPhoneのバックアップデータ(10年前~4年くらい前)をiBackupbotで読み取り、SafariブックマークをHTMLに書き出す。
iPhoneを定期的に初期化し清潔を保っていたため毎度UUIDが変わるためバックアップもちらかっていた。
およそ20個ほどのUUIDから20個のHTMLを作成。この時点で奇人。

PC上のとっちらかってるエクスポートブックマーク.htmlをEverythingで30個ほど見つける。

Edge, Firefox, Chrome, Ironの最新ブックマークをHTMLに書き出す。

type *.html > merge.txt

で適当に連結。12万行になる。

テキストエディタで掃除、整理整頓、ソート、フォルダ削除。簡易的な重複削除。1万行。

JavascriptでURLが全く同じであれば重複削除。5000行まで圧縮。

10年間で5000ブックマーク。暇人すぎる。

閑話休題。

目標1は
・フォルダ構造をぶっ壊してしまったため機会的にフォルダ分けして動作を軽くしなければならない。
・サブドメインを含めないドメイン名主体で整理。
・互換性のあるフォーマットでHTML出力。

ブックマークバー/アーカイブ/ドメイン頭文字/ドメイン/固有のブックマークという構造
(例)
ブックマークバー/アーカイブ/a/apple.com/<a href="https://music.apple.com">‎Apple Music</a>

目標2は
フェッチAPIでDNS解決可否、サーバからお返事があるのか聴く。
なければ
ブックマークバー/インターネット・アーカイブ/a/apple.com/<a href="https://web.archive.org/web/*/https://music.apple.com">[IA] Apple Music</a>
のリンクに差し替える。
(フェッチAPIが有用なのかはわからないけれど)

目標は今月末まで。

ざっと調べた限りでは
Location, URLインターフェイス他ネイティブで用意されているパーツも使えそうであるがサブドメインやら扱うのが難しい。
正規表現でやってみようと思ったが汚くなるのでやめた。

document.querySelectorAll("a")[50].hostname;
"foo.bar.com"

見立てでは初心者でもかんたんで少量の記述量で済むjQuery, URI.jsに頼って書くが楽そうだ。

プログラミングに関することも書いておかないと規約上いけませんので

let uri = new URI("http://abcd.efgh.example.org/foo/hello.html");

uri.subdomain(); 
"abcd.efgh"

uri.domain(); 
"example.org"

uri.tld();
"org"

uri.domain()[0]; 
"e"

JR東海のポスターのごとく「ラクだ!」

プロなら30分の仕事かもしれない。

URI.js - https://medialize.github.io/URI.js/docs.html

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

【お試し】クイック スタート:Visual Studio Code を使用して Azure で関数を作成する(JavaScript)

概要

クイック スタート:Visual Studio Code を使用して Azure で関数を作成するの内容を参考に、AzureFunctionsを触ってみます。

環境情報

2020/6/3時点

  • MacOS 10.15.5

- Node.js v12.18.0 (最新は14.4.0でしたがLTSがおすすめらしいのでLTSのバージョンにしました。)

事前準備

  • Microsoft Azureアカウント(無料)を作成しておく
    • アカウント作成時にGitHubアカウントでログインもしくは新規でメールアドレスを登録してアカウントを作成
    • 電話番号、クレジットカード情報の登録も必要
    • クレジットカード情報は、認証に利用し、アップグレードを叫ばない限り費用が発生することはないようです。
  • Visual Studio Codeを使うのでインストールしておく
    • 拡張機能としてAzure Functionsを使うのでインストールしておく
  • Node.jsをインストールしておく

ローカルプロジェクトを作成する

ローカルプロジェクトを作成するの内容をトレースしていきます。

上記記事と、最新の環境とで差があるようですが、概ね手順の通り進められます。

新しいプロジェクトの作成

スクリーンショット 2020-06-03 19.32.57.png

プロジェクトワークスペースのディレクトリを指定

これらの手順は、ワークスペースの外部で実行するように設計されています。 ここでは、ワークスペースに含まれるプロジェクト フォルダーは選択しないでください。

という注意書きがあるので、どういうことなのかやってみました。

もともと別のソースを管理するワークスペース「SRC」のワークスペースをそのまま選択して進めてみたところ、
既存のワークスペースに「HttpExample」というディレクトリだけでなく、そのディレクトリの外側に.gitignorehost.jsonなどいろいろなファイルが生成されてしまいました。

なので、既存のワークスペースがごちゃごちゃするから、新しいフォルダを指定してくださいね。
という注意書きのようですね。
スクリーンショット 2020-06-03 19.34.03.png
スクリーンショット 2020-06-03 19.43.49.png

新規フォルダを作成してそこを指定すると、きれいに関連するファイルだけがまとまって作成されました。
スクリーンショット 2020-06-03 19.50.24.png

プロンプトで次の情報を入力する

Select a languageJavaScriptを選択する。
スクリーンショット 2020-06-03 19.34.29.png

Select a template for your project's first functionHTTP triggerを選択する。
スクリーンショット 2020-06-03 19.35.33.png

Provide a function nameで任意のプロジェクト名を入力します。
ここでは、クイックスタートのサイトで指定されているHttpExampleという名前を指定していきます。
スクリーンショット 2020-06-03 19.36.04.png

Authorization levelAnonymousを指定する。
Anonymousに設定することで、APIキーが不要な単純なHTTPリクエストによるAzure Functionを体験できるようです。
これだとPublicに公開された誰でも実行できるものになってしまうということであれば、別の承認レベルを指定する必要がありそうですね。
スクリーンショット 2020-06-03 19.36.35.png

関数をローカルで実行する

なんと、ここでAzure Functions Core Toolsというツールを使って、関数のテストを行うようです。

事前準備で、VSCodeやらNode.jsをセットアップさせておきながら、まだ必要な環境があるとは。。。

Azure Functions Core Toolsのセットアップ

Macの環境なので、Terminalを起動してbrew tap azure/functionsを実行。

$ brew tap azure/functions
Updating Homebrew...
==> Auto-updated Homebrew!
Updated 1 tap (homebrew/core).
==> New Formulae
bombadillo                                                                             coredns
==> Updated Formulae
abcmidi      aws-cdk      awscli@1     composer     dnscontrol   elektra      flow         ghq          imagemagick  liblouis     node@10      rbspy        simple-scan
aliyun-cli   awscli       buildifier   csvq         doctl        eureka       geoserver    grin-wallet  ktlint       node         node@12      semgrep
==> Deleted Formulae
baidupcs-go

==> Tapping azure/functions
Cloning into '/usr/local/Homebrew/Library/Taps/azure/homebrew-functions'...
remote: Enumerating objects: 71, done.
remote: Counting objects: 100% (71/71), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 338 (delta 48), reused 41 (delta 29), pack-reused 267
Receiving objects: 100% (338/338), 50.35 KiB | 305.00 KiB/s, done.
Resolving deltas: 100% (184/184), done.
Tapped 4 formulae (31 files, 93.6KB).

続いてbrew install azure-functions-core-tools@3を実行。

$ brew install azure-functions-core-tools@3
==> Installing azure-functions-core-tools@3 from azure/functions
==> Downloading https://functionscdn.azureedge.net/public/3.0.2534/Azure.Functions.Cli.osx-x64.3.0.2534.zip
######################################################################## 100.0%

 Telemetry 
 --------- 
 The Azure Functions Core tools collect usage data in order to help us improve your experience.
 The data is anonymous and doesn't include any user specific or personal information. The data is collected by Microsoft.

 You can opt-out of telemetry by setting the FUNCTIONS_CORE_TOOLS_TELEMETRY_OPTOUT environment variable to '1' or 'true' using your favorite shell.
?  /usr/local/Cellar/azure-functions-core-tools@3/3.0.2534: 5,114 files, 564.9MB, built in 21 seconds

VSCodeでF5キーを押下

F5キーを押下すると、こんな感じになります。

スクリーンショット 2020-06-03 20.09.51.png

Azure Functions Core Toolsをインストールしたからこんな感じになるのか、インストールしなくても実は動くのか。。。インストールする前に試せばよかったです。

とりあえずTERMINALに出力された結果としては以下のような感じでした。

> Executing task: npm install <

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN azurefunctions@1.0.0 No description
npm WARN azurefunctions@1.0.0 No repository field.
npm WARN azurefunctions@1.0.0 No license field.

up to date in 0.653s
found 0 vulnerabilities


Terminal will be reused by tasks, press any key to close it.

> Executing task: func host start <


                  %%%%%%
                 %%%%%%
            @   %%%%%%    @
          @@   %%%%%%      @@
       @@@    %%%%%%%%%%%    @@@
     @@      %%%%%%%%%%        @@
       @@         %%%%       @@
         @@      %%%       @@
           @@    %%      @@
                %%
                %

Azure Functions Core Tools (3.0.2534 Commit hash: bc1e9efa8fa78dd1a138dd1ac1ebef97aac8d78e)
Function Runtime Version: 3.0.13353.0
[2020/06/03 11:08:07] FUNCTIONS_WORKER_RUNTIME set to node. Skipping WorkerConfig for language:python
[2020/06/03 11:08:07] FUNCTIONS_WORKER_RUNTIME set to node. Skipping WorkerConfig for language:java
[2020/06/03 11:08:07] FUNCTIONS_WORKER_RUNTIME set to node. Skipping WorkerConfig for language:powershell
[2020/06/03 11:08:07] Building host: startup suppressed: 'False', configuration suppressed: 'False', startup operation id: 'd6b60201-83fa-46d6-8a79-67bd2d429035'
[2020/06/03 11:08:08] Reading host configuration file '/Users/you_name_is_yu/Develop/AzureFunctions/host.json'
[2020/06/03 11:08:08] Host configuration file read:
[2020/06/03 11:08:08] {
[2020/06/03 11:08:08]   "version": "2.0",
[2020/06/03 11:08:08]   "extensionBundle": {
[2020/06/03 11:08:08]     "id": "Microsoft.Azure.Functions.ExtensionBundle",
[2020/06/03 11:08:08]     "version": "[1.*, 2.0.0)"
[2020/06/03 11:08:08]   }
[2020/06/03 11:08:08] }
[2020/06/03 11:08:08] Reading functions metadata
[2020/06/03 11:08:08] 1 functions found
[2020/06/03 11:08:08] Looking for extension bundle Microsoft.Azure.Functions.ExtensionBundle at /var/folders/pf/mbns7g5n6nlfj48466cy3kj40000gn/T/Functions/ExtensionBundles/Microsoft.Azure.Functions.ExtensionBundle
[2020/06/03 11:08:08] Fetching information on versions of extension bundle Microsoft.Azure.Functions.ExtensionBundle available on https://functionscdn.azureedge.net/public/ExtensionBundles/Microsoft.Azure.Functions.ExtensionBundle/index.json
[2020/06/03 11:08:09] Downloading extension bundle from https://functionscdn.azureedge.net/public/ExtensionBundles/Microsoft.Azure.Functions.ExtensionBundle/1.1.1/Microsoft.Azure.Functions.ExtensionBundle.1.1.1.zip to /var/folders/pf/mbns7g5n6nlfj48466cy3kj40000gn/T/62e2229d-4a18-4dca-92f3-7f12aef4f16e/Microsoft.Azure.Functions.ExtensionBundle.1.1.1.zip
[2020/06/03 11:08:13] Completed downloading extension bundle from https://functionscdn.azureedge.net/public/ExtensionBundles/Microsoft.Azure.Functions.ExtensionBundle/1.1.1/Microsoft.Azure.Functions.ExtensionBundle.1.1.1.zip to /var/folders/pf/mbns7g5n6nlfj48466cy3kj40000gn/T/62e2229d-4a18-4dca-92f3-7f12aef4f16e/Microsoft.Azure.Functions.ExtensionBundle.1.1.1.zip
[2020/06/03 11:08:13] Extracting extension bundle at /var/folders/pf/mbns7g5n6nlfj48466cy3kj40000gn/T/Functions/ExtensionBundles/Microsoft.Azure.Functions.ExtensionBundle/1.1.1
[2020/06/03 11:08:13] Zip extraction complete
[2020/06/03 11:08:13] Loading Extention bundle from /var/folders/pf/mbns7g5n6nlfj48466cy3kj40000gn/T/Functions/ExtensionBundles/Microsoft.Azure.Functions.ExtensionBundle/1.1.1
[2020/06/03 11:08:13] FUNCTIONS_WORKER_RUNTIME set to node. Skipping WorkerConfig for language:python
[2020/06/03 11:08:13] FUNCTIONS_WORKER_RUNTIME set to node. Skipping WorkerConfig for language:java
[2020/06/03 11:08:13] FUNCTIONS_WORKER_RUNTIME set to node. Skipping WorkerConfig for language:powershell
[2020/06/03 11:08:13] Initializing Warmup Extension.
[2020/06/03 11:08:14] Initializing Host. OperationId: 'd6b60201-83fa-46d6-8a79-67bd2d429035'.
[2020/06/03 11:08:14] Host initialization: ConsecutiveErrors=0, StartupCount=1, OperationId=d6b60201-83fa-46d6-8a79-67bd2d429035
[2020/06/03 11:08:14] LoggerFilterOptions
[2020/06/03 11:08:14] {
[2020/06/03 11:08:14]   "MinLevel": "None",
[2020/06/03 11:08:14]   "Rules": [
[2020/06/03 11:08:14]     {
[2020/06/03 11:08:14]       "ProviderName": null,
[2020/06/03 11:08:14]       "CategoryName": null,
[2020/06/03 11:08:14]       "LogLevel": null,
[2020/06/03 11:08:14]       "Filter": "<AddFilter>b__0"
[2020/06/03 11:08:14]     },
[2020/06/03 11:08:14]     {
[2020/06/03 11:08:14]       "ProviderName": "Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.SystemLoggerProvider",
[2020/06/03 11:08:14]       "CategoryName": null,
[2020/06/03 11:08:14]       "LogLevel": "None",
[2020/06/03 11:08:14]       "Filter": null
[2020/06/03 11:08:14]     },
[2020/06/03 11:08:14]     {
[2020/06/03 11:08:14]       "ProviderName": "Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.SystemLoggerProvider",
[2020/06/03 11:08:14]       "CategoryName": null,
[2020/06/03 11:08:14]       "LogLevel": null,
[2020/06/03 11:08:14]       "Filter": "<AddFilter>b__0"
[2020/06/03 11:08:14]     }
[2020/06/03 11:08:14]   ]
[2020/06/03 11:08:14] }
[2020/06/03 11:08:14] FunctionResultAggregatorOptions
[2020/06/03 11:08:14] {
[2020/06/03 11:08:14]   "BatchSize": 1000,
[2020/06/03 11:08:14]   "FlushTimeout": "00:00:30",
[2020/06/03 11:08:14]   "IsEnabled": true
[2020/06/03 11:08:14] }
[2020/06/03 11:08:14] SingletonOptions
[2020/06/03 11:08:14] {
[2020/06/03 11:08:14]   "LockPeriod": "00:00:15",
[2020/06/03 11:08:14]   "ListenerLockPeriod": "00:00:15",
[2020/06/03 11:08:14]   "LockAcquisitionTimeout": "10675199.02:48:05.4775807",
[2020/06/03 11:08:14]   "LockAcquisitionPollingInterval": "00:00:05",
[2020/06/03 11:08:14]   "ListenerLockRecoveryPollingInterval": "00:01:00"
[2020/06/03 11:08:14] }
[2020/06/03 11:08:14] HttpOptions
[2020/06/03 11:08:14] {
[2020/06/03 11:08:14]   "DynamicThrottlesEnabled": false,
[2020/06/03 11:08:14]   "MaxConcurrentRequests": -1,
[2020/06/03 11:08:14]   "MaxOutstandingRequests": -1,
[2020/06/03 11:08:14]   "RoutePrefix": "api"
[2020/06/03 11:08:14] }
[2020/06/03 11:08:14] Starting JobHost
[2020/06/03 11:08:14] Starting Host (HostId=younameisyupc-237003343, InstanceId=309bf51b-da3c-4db1-be93-1afdc15b677e, Version=3.0.13353.0, ProcessId=5908, AppDomainId=1, InDebugMode=False, InDiagnosticMode=False, FunctionsExtensionVersion=(null))
[2020/06/03 11:08:14] Loading functions metadata
[2020/06/03 11:08:14] 1 functions loaded
[2020/06/03 11:08:14] Loading proxies metadata
[2020/06/03 11:08:14] Initializing Azure Function proxies
[2020/06/03 11:08:16] 0 proxies loaded
[2020/06/03 11:08:16] Starting worker process:node  --inspect=9229 "/usr/local/Cellar/azure-functions-core-tools@3/3.0.2534/workers/node/dist/src/nodejsWorker.js" --host 127.0.0.1 --port 50181 --workerId c50406b7-d0c6-48fa-a29d-c66ec65f8d4f --requestId 83f76573-42dc-499a-80b9-a44874849ea9 --grpcMaxMessageLength 134217728
[2020/06/03 11:08:17] node process with Id=5919 started
[2020/06/03 11:08:17] Generating 1 job function(s)
[2020/06/03 11:08:17] Found the following functions:
[2020/06/03 11:08:17] Host.Functions.HttpExample
[2020/06/03 11:08:17] 
[2020/06/03 11:08:17] Initializing function HTTP routes
[2020/06/03 11:08:17] Mapped function route 'api/HttpExample' [get,post] to 'HttpExample'
[2020/06/03 11:08:17] 
[2020/06/03 11:08:17] Host initialized (2741ms)
[2020/06/03 11:08:17] Host started (2750ms)
[2020/06/03 11:08:17] Job host started
[2020/06/03 11:08:17] Debugger listening on ws://127.0.0.1:9229/d7c34f05-e633-4dec-88f4-140ed939a894
[2020/06/03 11:08:17] For help, see: https://nodejs.org/en/docs/inspector
Hosting environment: Production
Content root path: /Users/you_name_is_yu/Develop/AzureFunctions
Now listening on: http://0.0.0.0:7071
Application started. Press Ctrl+C to shut down.

Http Functions:

        HttpExample: [GET,POST] http://localhost:7071/api/HttpExample

[2020/06/03 11:08:17] Worker c50406b7-d0c6-48fa-a29d-c66ec65f8d4f connecting on 127.0.0.1:50181
[2020/06/03 11:08:22] Host lock lease acquired by instance ID '000000000000000000000000354A045E'.
[2020/06/03 11:08:23] Debugger attached.

気になるポイントとしてはこんな感じで、HttpFunctionが動き始めたようなメッセージが出力されている部分でしょうか。

http://localhost:7071/api/HttpExample

ただ、このURLのままアクセスしてもダメみたいで、以下のように、クエリパラメータとしてname=Functionsを指定する必要があるようです。
http://localhost:7071/api/HttpExample?name=Functions
スクリーンショット 2020-06-03 20.13.50.png

アクセスしてみると、よくあるHello Worldのような感じで、Hello Functionsという文字が出力されました。

ブラウザにアクセスした時点でTERMINALに出力される内容としては以下のような内容が出力されています。

[2020/06/03 11:13:27] Executing HTTP request: {
[2020/06/03 11:13:27]   "requestId": "816dc4fe-2914-4e86-99a5-7820ab231808",
[2020/06/03 11:13:27]   "method": "GET",
[2020/06/03 11:13:27]   "uri": "/api/HttpExample"
[2020/06/03 11:13:27] }
[2020/06/03 11:13:27] Executing 'Functions.HttpExample' (Reason='This function was programmatically called via the host APIs.', Id=32b32a60-2b58-4988-8fd5-329484b2a50d)
[2020/06/03 11:13:28] JavaScript HTTP trigger function processed a request.
[2020/06/03 11:13:28] Executed 'Functions.HttpExample' (Succeeded, Id=32b32a60-2b58-4988-8fd5-329484b2a50d)
[2020/06/03 11:13:28] Executed HTTP request: {
[2020/06/03 11:13:28]   "requestId": "816dc4fe-2914-4e86-99a5-7820ab231808",
[2020/06/03 11:13:28]   "method": "GET",
[2020/06/03 11:13:28]   "uri": "/api/HttpExample",
[2020/06/03 11:13:28]   "identities": [
[2020/06/03 11:13:28]     {
[2020/06/03 11:13:28]       "type": "WebJobsAuthLevel",
[2020/06/03 11:13:28]       "level": "Admin"
[2020/06/03 11:13:28]     }
[2020/06/03 11:13:28]   ],
[2020/06/03 11:13:28]   "status": 200,
[2020/06/03 11:13:28]   "duration": 1095
[2020/06/03 11:13:28] }
[2020/06/03 11:13:28] Executing HTTP request: {
[2020/06/03 11:13:28]   "requestId": "ec5ffac0-d06e-4795-ae24-e2bb5b2f9cd7",
[2020/06/03 11:13:28]   "method": "GET",
[2020/06/03 11:13:28]   "uri": "/favicon.ico"
[2020/06/03 11:13:28] }
[2020/06/03 11:13:28] Executed HTTP request: {
[2020/06/03 11:13:28]   "requestId": "ec5ffac0-d06e-4795-ae24-e2bb5b2f9cd7",
[2020/06/03 11:13:28]   "method": "GET",
[2020/06/03 11:13:28]   "uri": "/favicon.ico",
[2020/06/03 11:13:28]   "identities": [],
[2020/06/03 11:13:28]   "status": 404,
[2020/06/03 11:13:28]   "duration": 352
[2020/06/03 11:13:28] }

デバッガーを終了

Disconnectボタンを押下するか、Ctrl+Cを押下することでデバッグを終了します。
スクリーンショット 2020-06-03 20.17.38.png

Azureへのサインイン

Azure Functionsのアプリを発行するには、Azureへのサインインが必要です。
スクリーンショット 2020-06-03 20.19.43.png

ブラウザでのログインが求められたので、私の場合はGitHubアカウントでサインインします。
スクリーンショット 2020-06-03 20.20.29.png

サインインしてしまえば、表示されたブラウザは閉じてOKです。
スクリーンショット 2020-06-03 20.21.40.png

VSCodeでは無料試用版という表示になっているんですね。
スクリーンショット 2020-06-03 20.23.05.png

Azure にプロジェクトを発行する

スクリーンショット 2020-06-03 20.24.02.png

Create new Function App in Azureを選択する。
※Advancedは選択しないでとのこと。
スクリーンショット 2020-06-03 20.26.12.png

関数にユニークな任意の名前を指定します。
スクリーンショット 2020-06-03 20.28.25.png

関数の名前にはアンダーバーなど使えない文字があるようです。
スクリーンショット 2020-06-03 20.28.30.png

今回はNode.jsのバージョンを12.18.0としているため、Node.js 12を選択します。
スクリーンショット 2020-06-03 20.28.49.png

Japan Eastを選択します。

パフォーマンスを向上させるために、お近くのリージョンを選択してください

とのことです。
スクリーンショット 2020-06-03 20.29.24.png

上記手順まで進むと、処理が進みます。
スクリーンショット 2020-06-03 20.30.18.png

完了すると以下のようになります。
View outputボタンを押下することで、作成とデプロイの結果を確認することができます。
スクリーンショット 2020-06-03 20.35.21.png

8:30:46 PM: Creating resource group "yuyamaguchifunctionshttp" in location "japaneast"...
8:30:46 PM: Successfully created resource group "yuyamaguchifunctionshttp".
8:30:46 PM: Creating storage account "yuyamaguchifunctionshttp" in location "japaneast" with sku "Standard_LRS"...
8:31:18 PM: Successfully created storage account "yuyamaguchifunctionshttp".
8:31:18 PM: Verifying that Application Insights is available for this location...
8:31:18 PM: Creating Application Insights resource "yuyamaguchifunctionshttp"...
8:31:21 PM: Successfully created Application Insights resource "yuyamaguchifunctionshttp".
8:31:21 PM: Creating new function app "Yu-Yamaguchi-Functions-HttpExample"...
8:32:09 PM: Successfully created function app "Yu-Yamaguchi-Functions-HttpExample": https://yu-yamaguchi-functions-httpexample.azurewebsites.net
8:32:13 PM Yu-Yamaguchi-Functions-HttpExample: Creating zip package...
8:32:13 PM Yu-Yamaguchi-Functions-HttpExample: Starting deployment...
8:32:22 PM Yu-Yamaguchi-Functions-HttpExample: Updating submodules.
8:32:22 PM Yu-Yamaguchi-Functions-HttpExample: Preparing deployment for commit id 'a320bfa83a'.
8:32:23 PM Yu-Yamaguchi-Functions-HttpExample: Skipping build. Project type: Run-From-Zip
8:32:23 PM Yu-Yamaguchi-Functions-HttpExample: Skipping post build. Project type: Run-From-Zip
8:32:23 PM Yu-Yamaguchi-Functions-HttpExample: Triggering recycle (preview mode disabled).
8:32:27 PM Yu-Yamaguchi-Functions-HttpExample: Syncing 2 function triggers with payload size 158 bytes successful.
8:32:28 PM Yu-Yamaguchi-Functions-HttpExample: Deployment successful.
8:32:42 PM Yu-Yamaguchi-Functions-HttpExample: Started postDeployTask "npm install".
8:32:43 PM Yu-Yamaguchi-Functions-HttpExample: Querying triggers...
8:32:48 PM Yu-Yamaguchi-Functions-HttpExample: HTTP Trigger Urls:
  HttpExample: https://yu-yamaguchi-functions-httpexample.azurewebsites.net/api/HttpExample

Azure で関数を実行する

URLをコピーする。
スクリーンショット 2020-06-03 20.39.48.png

コピーしたURLは以下の通りです。
https://yu-yamaguchi-functions-httpexample.azurewebsites.net/api/HttpExample

これにローカル環境での動作確認でも指定したクエリ文字列?name=Functionsを指定したURLを生成し、ブラウザでアクセスします。
https://yu-yamaguchi-functions-httpexample.azurewebsites.net/api/HttpExample?name=Functions
スクリーンショット 2020-06-03 20.41.59.png

このURLをみる限り、ユニークなFunctionの名前とは、グローバルでのユニークとなるように指定する必要があるようですね。
無闇に作らず、命名には注意が必要そうです。

Azureのポータルサイトにアクセスすると、デプロイしたFunctionが確認できます。

スクリーンショット 2020-06-03 20.44.09.png

Azure Functionsはリクエスト数が多いとその分費用?がかかるような従量課金だと思いますので、ここで作ったFunctionは消しておきますw
なので、ここに載せているFunctionのURLにアクセスしても意味が無いので悪しからずm(_ _)m

以上で、クイックスタートの内容は完了です。
まだ何ができるかわかっていませんが、できればOffice365とか会社で使っているので、TeamsとかOneDriveのドキュメントとか、その辺りといい感じのことができると嬉しいなと思っています。

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

Javascriptで値が数値かどうかチェック→スペースかどうかもチェック

Wano株式会社で社内のもろもろを担当しているakibinです。

ヒゲ男ばかり聴いて、少しでも若返ったつもりでいる今日このごろです。でも好きだなーこのちょっと縦ノリっぽい楽曲と声。

今回やってみたこと

Javascriptで値が数値かどうかチェックするのにこちらの記事を参照させてもらいました!(分かりやすかったです!!)

がしかし!

スペースを数字と認識してしまうので、スペースかどうかもチェックするようにしてみました。

こんな感じ

正規表現の\S(空白文字以外の文字)と、!で論理値を反転させてif文で判定。当てはまった(true)場合はtry…catch文を使用して終了する。(JSにはexitが無いので)
※htmlは参照させていただいた記事と変わりないのですが、念の為載せておきます。

check.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>check</title>
  <script src="check.js"></script>
</head>
<body>
  数値を入力して下さい:
  <input type="text" id="input01">
  <input type="button" value="check" onclick="check();">
</body>
</html>
check.js
'use strict';

var check = function() {
    var num = document.getElementById('input01').value;

    try {
        if (!num.match(/\S/g)){
            alert("スペースが入ってますよ!");
            throw new Error("スペースありのため終了します");
        }

        if(!isNaN(num)){
            alert("入力された数は" + num + "です。");
        } else {
              alert("数値以外が入力されています");
        }
    } catch(e) {
        console.log(e.message);
    }
}

これで…

これでOKぃ!!

こちらもよろしくです。
****************************************
◆ Twitterアカウント
@AkibinMusic

◆ Youtubeチャンネル
https://www.youtube.com/channel/UC-JOpwEnJn3gCrUA4NdCYgg

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

Javascriptで値が数値かどうかチェック→スペースかどうかも

Wano株式会社で社内のもろもろを担当しているakibinです。

ヒゲ男ばかり聴いて、少しでも若返ったつもりでいる今日このごろです。でも好きだなーこのちょっと縦ノリっぽい楽曲と声。

今回やってみたこと

Javascriptで値が数値かどうかチェックするのにこちらの記事を参照させてもらいました!(分かりやすかったです!!)

がしかし!

スペースを数字と認識してしまうので、スペースかどうかもチェックするようにしてみました。

こんな感じ

正規表現の\S(空白文字以外の文字)と、!で論理値を反転させてif文で判定。当てはまった(true)場合はtry…catch文を使用して終了する。(JSにはexitが無いので)
※htmlは参照させていただいた記事と変わりないのですが、念の為載せておきます。

check.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>check</title>
  <script src="check.js"></script>
</head>
<body>
  数値を入力して下さい:
  <input type="text" id="input01">
  <input type="button" value="check" onclick="check();">
</body>
</html>
check.js
'use strict';

var check = function() {
    var num = document.getElementById('input01').value;

    try {
        if (!num.match(/\S/g)){
            alert("スペースが入ってますよ!");
            throw new Error("スペースありのため終了します");
        }

        if(!isNaN(num)){
            alert("入力された数は" + num + "です。");
        } else {
              alert("数値以外が入力されています");
        }
    } catch(e) {
        console.log(e.message);
    }
}

これで…

これでOKぃ!!

こちらもよろしくです。
****************************************
◆ Twitterアカウント
@AkibinMusic

◆ Youtubeチャンネル
https://www.youtube.com/channel/UC-JOpwEnJn3gCrUA4NdCYgg

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

JavaScriptでhtmlのTableをいじる

初めに

今回はjQueryをメインでDOM操作しながらTableをあれこれする方法を展開します。

コード

セルを押下できるようにする

セルを押下するとイベントが発火して処理される仕組み(今回はアラート表示)
まずはhtml側にTableをセットする(今回はタイトルなしです)

test.html
<table border="1">
        <tr>
            <td id="a"><a onclick="onClick(this);">Test1</a></td>
            <td id="b"><a onclick="onClick(this);">Test2</a></td>
            <td id="c"><a onclick="onClick(this);">Test3</a></td>
        </tr>
        <tr>
            <td id="d"><a onclick="onClick(this);">Test4</a></td>
            <td id="e"><a onclick="onClick(this);">Test5</a></td>
            <td id="f"><a onclick="onClick(this);">Test6</a></td>
        </tr>
    </table>
main.js
function onClick(link){
    alert("Test");
}

押下したセルの文字を入れ替え

main.js
function onClick(link){
    $(link).html("入れ替え")
}

押下したセルの文字を抽出

main.js
function onClick(link){
    $(link).parent().text()
}

押下したセルの列番号を抽出

main.js
function onClick(link){
    $(link).parent().index()
}

押下したセルのIDを抽出

idを振っている前提です

main.js
function onClick(link){
    $(link).parent().attr('id')
}

終わりに

今回はTableについての展開でした。
すごく簡単ですね!楽しい!

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

難読化されたJavaScriptを解読するツールを作った

Obfuscator.ioというJavaScriptコードを難読化できるツールがあります。

例えば以下のような感じ

Before
難読化前

After
難読化されたコード

正直難読化されてても慣れればコードを読むのはそんなに苦じゃないのですが、自動ですべて解読するツールを作ったらおもしろそうだと思ってやってみました。

難読化されたJavaScriptを半自動で解読するツール

解読ツール

Vue.js(サイト側)とCloud Functions(API側)で作ってみました。

公開URL: https://sigr.io/deobfuscator/

残念ながら完全自動化までは至りませんでしたが、最初にターゲットとなる関数名を入力すれば後は自動で解析してくれます。

ツールの使い方

  1. 解読したいコードをInputに貼り付ける。
  2. 貼り付けたコードの中に以下のような部分があるので関数名をコピーする(3つのうちどれかに似てるはず)

_0x439c[103]
_0x439c('0x4')
_0x439c('0x2','f]Xg')

これの場合は_0x439cが対象(関数名)。Target function nameの欄にコピーしたのを貼り付ける

3.「Deobfuscate」をクリック
4.ちゃんと解読されればResultに結果が表示される

ちゃんと解読されると難読化された部分が読めるようになるはずです。
結果

さいごに

GitHubにフロント側のソースコードをうpしています。
https://github.com/LostMyCode/javascript-deobfuscator

CloudFunctionsで動いているAPI側のコードもいつか公開するかもしれません。

javascript-deobfuscator

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

google.script.run が Promise を返すようにする

期待を返せ

google.script.run は withSuccessHandler や withFailureHandler でコールバックを登録する仕様となっており、 Promise を返してくれません。
async / await ですっきり書きたいですよね。

理想
(async ()=> {
  const data1 = await google.script.run.myFunction();
  const data2 = await google.script.run.myFunction2(data1);
  console.log(data1);
  console.log(data2);
})();
現実
google.script.run
.withSuccessHandler( data1 => {
  google.script.run
  .withSuccessHandler( data2 => {
    console.log(data1);
    console.log(data2);
  })
  .withFailureHandler(console.error)
  .myFunction2(data1);
})
.withFailureHandler(console.error)
.myFunction();

Promiseを返さないなら自作関数で包んでしまえ

というわけで google.script.run を元に Promise を返す関数を動的に定義します。

const ServerScript = new class {
  constructor() {
    const createAsyncFunction = (methodName) => function() {
      return new Promise( (resolve, reject) => {
        google.script.run
        .withSuccessHandler(resolve)
        .withFailureHandler(reject)
        [methodName]
        .apply(null, arguments);
      });
    }.bind(this);
    for (const methodName in google.script.run) {
      const type = google.script.run[methodName].prototype.constructor.name;
      if (type != methodName) {
        this[methodName] = createAsyncFunction(methodName);
      }
    }
  }
};
(async ()=> {
  const data1 = await ServerScript.myFunction();
  const data2 = await ServerScript.myFunction2(data1);
  console.log(data1);
  console.log(data2);
})();

for 文の中の if は withSuccessHandler や withFailureHandler を弾くためのものなので、別に無くても問題ないです。呼ばなければ良い話ですから。

また、この条件でも doGet などが入ってきてしまうのですが、呼んでもエラーなどは起きずに undefined が返ってきます。元々の google.script.run からそのような動きをするようなので、こちらも特に気にしないことにします。

以上、快適なGASライフを!

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

webpack3からwebpack4へバージョンアップ

保守できていなかったwebpackのバージョンをあげた際の記録です。
メモレベルで恐縮ですが、よかったら参考にしてください。

モチベーション

  • 最新のTypeScriptを導入したい
  • ビルドを速くしたい

移行手順

基本的にはこちらのマイグレーションガイド通りです。
https://webpack.js.org/migrate/4/

  • package.jsonの修正
  • modeの追加
  • pluginの設定見直し
  • loaderのバージョンアップ

移行手順詳細

package.jsonの修正

https://github.com/webpack/webpack/releases
こちらを参考にwebpackの箇所のバージョンを修正しました。

package.json
"webpack": "^4.42.0"
npm install

yarnの方はyarn installしてください。

modeの追加

productiondevelopmentの二つがありwebpack.config.jsに記載しないと警告がでます。
本番用と開発用ですね。
圧縮されるかどうかなどの違いがあります。

webpack.config.js
module.exports = {
//...
  mode: 'production',
//...
}

pluginの設定見直し

3rd partyのプラグインのバージョンアップや一部機能がデフォルトになったため(modeの記述でOKになったため)見直しをしました。

before

webpack.config.js
module.exports = {
//...
  plugins: [
    new webpack.optimize.UglifyJsPlugin({})
  ]
//...
}

after

webpack.config.js
module.exports = {
//...
  mode: 'production'
//...
}

loaderのバージョンアップ

Cannot read property 'babel' of undefined

ビルドすると、上記のログが出たのでbabel-loaderをバージョンアップしました。
最新版にしたかったのですが、babelのバージョンアップとは切り分けたかったので

package.json
"babel-loader": "^7.1.5"

で対応しました。
babel-loaderの8系移行を使うには
https://koukitips.net/if-you-raise-the-babel-loader-to-v8-the-solution-is-when-you-get-an-error/
この辺りを参考にする必要がありそうです。

参考

https://webpack.js.org/migrate/4/
https://thebrainfiles.wearebrain.com/moving-from-webpack-3-to-webpack-4-f8cdacd290f9
https://auth0.com/blog/webpack-4-release-what-is-new/#L7--Faster-Build-Times

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

webpack1からwebpack3へバージョンアップ

保守できていなかったwebpackのバージョンをあげた際の記録です。
メモレベルで恐縮ですが、よかったら参考にしてください。

モチベーション

  • TypeScriptを導入したい(1だとts-loaderが対応していない)
  • ブラウザでのjs実行を速くしたい

なぜ2ではなく、3なの?

https://webpack.js.org/migrate/3/
公式で

Note that there were far fewer changes between 2 and 3, so that migration shouldn't be too bad. If you are running into issues, please see the changelog for details.

となっていて、移行ガイドも
To v2 or v3 from v1
となっているから。

また、
https://medium.com/webpack/webpack-3-official-release-15fd2dd8f07b

Migrating from webpack 2 to 3, should involve no effort beyond running the upgrade commands in your terminal. We marked this as a Major change because of internal breaking changes that could affect some plugins.

とも。

使用ライブラリ

  • Grunt
  • Webpack
  • Bower

移行手順

  • package.jsonの修正
  • bower対応
  • DedupePlugin,OccurenceOrderPlugin
  • grunt-webpackバージョンアップ
  • moduleDirectories
  • resolve
  • module
  • debug

移行手順詳細

package.jsonの修正

webpackの箇所のバージョンを修正

package.json
"webpack": "^3.12.0"
npm install

yarnの方はyarn installしてください

bower対応

bowerとは、公式で非推奨になっていますが、フロントエンド周りのパッケージマネージャーです。
将来的使わないようにしたいですが、今回はwebpackのバージョンアップに専念しています。

webpack.config.js
new webpack.ResolverPlugin(
  new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin("bower.json", ["main"])
)

上記はwebpackでbower管理のファイルを読み込む際のお作法のようなものです。

https://github.com/webpack/webpack/issues/2324

It was removed. There is now a option resolve.plugins. But for bower you don't need a plugin anymore, as the resolve options has been expanded.
See: https://gist.github.com/sokra/27b24881210b56bbaff7#resolving-options

に書かれていますが、

webpack.config.js
descriptionFiles: ["package.json", "bower.json"]

をwebpack.config.jsのresolveに書いて対応しました。

参考

http://js.studio-kingdom.com/webpack/getting_started/using_plugins

DedupePlugin,OccurenceOrderPlugin

https://teratail.com/questions/93933

OccurenceOrderPluginは、Webpack2でOccurrenceOrderPlugin(rが2つ)と改名されています(Issue)。
DedupeはWebpack2ではデフォルトの動作となったので、プラグインとしては不要です。

とのこと。

grunt-webpackバージョンアップ

ビルドすると以下のエラーがでました。

configuration has an unknown property 'failOnError'. These properties are valid:

こちらはバージョンアップで対応しました。

package.json
"grunt-webpack": "^3.1.3"

参考

https://github.com/webpack-contrib/grunt-webpack/issues/81

moduleDirectories

ビルドすると以下のエラーがでました。

configuration.resolve has an unknown property 'modulesDirectories'. These properties are valid

https://webpack.js.org/migrate/3/#resolveroot-resolvefallback-resolvemodulesdirectories
公式ガイドのresolve.modulesDirectoriesを参考に修正しました。

before

webpack.config.js
resolve: {
  root: path.resolve(__dirname, "../js/dirname/"),
  modulesDirectories: [
    path.resolve(__dirname, "node_modules"),
    path.resolve(__dirname, "bower_components")
  ]
},

after

webpack.config.js
resolve: {
  modules: [
    path.resolve(__dirname, "../js/dirname/"),
    path.resolve(__dirname, "node_modules"),
    path.resolve(__dirname, "bower_components")
  ],
  descriptionFiles: ["package.json", "bower.json"]
},

descriptionFilesは前述したbower対応です。
同様にresolveloaderでもmodulesDirectoriesを使用していたので、modulesにリネームしました。

module.loaders

https://webpack.js.org/migrate/3/#moduleloaders-is-now-modulerules
こちらに沿って変更しました。

https://webpack.js.org/migrate/3/#automatic--loader-module-name-extension-removed
また、ドキュメントにあるように

webpack.config.js
loader: "babel"

ようにサフィックスを省略してはいけなくなり

webpack.config.js
loader: "babel-loader

のようにしました。

debug

https://webpack.js.org/migrate/3/#debug
こちらに沿って変更しました。

before

webpack.config.js
  debug: true,

after

webpack.config.js
  plugins: [
    new webpack.LoaderOptionsPlugin({
      debug: true
    })
  ]

参考

公式移行ガイド

https://webpack.js.org/migrate/3/#moduleloaders-is-now-modulerules

他にも

https://qiita.com/uggds/items/2ee337c5843aae28a34a
https://blog.hiroppy.me/entry/2017/02/03/212817

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

【Javascript】 2つの配列に格納されているオブジェクト内部要素を比較し、新しいオブジェクト要素を追加

背景

vueでaxios使ってAPI情報取得したときに、使うことが多いかもなーってことで、もっと良い方法がありそうだが、ひとまず、自分用にメモで取っておきます。

vueでAPI叩いたときに、配列内部にオブジェクトを抱えたものが返ってくる前提です。

もっとこのほうがいいなど、あればアドバイスいただければ幸いです。

内容

結論

内容
// axios叩いたときに返って来るdataの文字列
var obj = "data"
var test02 = [
    {
        "site_no": "00002",
        "term_id": "00002",
    },
    {
        "site_no": "00001",
        "term_id": "00001",
    },
    {
        "site_no": "00003",
        "term_id": "800003",
    },
    {
        "site_no": "00004",
        "term_id": "00004",
    }
]

// axios叩いたときに返ってくる配列(配列に格納されたオブジェクト一覧が返ってくる前提)
var test08 = [
    {
        "config":{
            "test01": "hoge1"  
        },
        "data": {
            "image_url": "http://test01",
            "term_id": "800001",
        },
        "header": {
            "aafa": "http://test01",
            "gdsa": "800001",
        },
        "status": 200
    },
    {
        "config":{
            "test01": "hoge1"  
        },
        "data": {
            "image_url": "http://test03",
            "term_id": "800003",
        },
        "header": {
            "aafa": "http://test01",
            "gdsa": "800001",
        },
        "status": 200
    },
    {
        "config":{
            "test01": "hoge1"  
        },
        "data": {
            "image_url": "http://test02",
            "term_id": "800002",
        },
        "header": {
            "aafa": "http://test01",
            "gdsa": "800001",
        },
        "status": 200
    }
]



// 比較する配列内部のオブジェクトに一致する文字列があるかを確認し、ある・ないで処理を分ける
// (2つ配列の内部どちらにもオブジェクトがあり、オブジェクト内部の特定文字列が存在することを確認する)
// ある:比較対象側オブジェクト(端末一覧情報オブジェクト)に、要素を追加
// ない:特に処理をせず終了
test08.some((val01)=>{
    test02.some((val02) => {
        console.log("val01: ", val01)
        if(obj !== ""){
            if( ( "term_id" in val01[obj] && "term_id" in val02 ) && ( val01[obj]["term_id"] === val02["term_id"] ) ){
                // console.log("存在します。")
                val02.image_url = val01[obj].image_url
            }else{
                // console.log("存在しません。")
            }
        }else{
            // オブジェクト内部に、検査したい要素があること確認 && それぞれのオブジェクトで要素が一致すること確認
            if( ( "term_id" in val01 && "term_id" in val02 ) && ( val01["term_id"] === val02["term_id"] ) ){
                // console.log("存在します。")
                // 条件に合致した場合に、片方の配列のオブジェクト要素に代入
                val02.image_url = val01.image_url
                // 合致したタイミングで処理を抜ける
                return true;
            }else{
                // console.log("存在しません。")
            }
        }
    })
})
console.log("result: ", test02)
結果
result:  [
  { site_no: '00002', term_id: '00002' },
  { site_no: '00001', term_id: '00001' },
  { site_no: '00003', term_id: '800003', image_url: 'http://test03' },
  { site_no: '00004', term_id: '00004' }
]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascriptでインターバルタイマーを作ってみた。

javascriptでインターバルタイマーを作ってみた。

仕様
・1秒ごとに音がなる。
・フォームに秒数を入れると、入力された秒数毎に別の音がなる。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>インターバルタイマー</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<style>
   h1{
      font-size:24px;
   }
#PassageArea{
   font-size:42px;
   border:1px solid #666;
   padding:20px;
   width:100px;
   margin:0px auto;
}
</style>

</head>
<body>

<div class="container mt-5" style="text-align:center;">

   <h1>インターバルタイマー</h1>

   <div class="container mt-5">
      <div id="PassageArea">0</div>
   </div>

   <div class="container mt-5">
      <input type="number" id="number" min=1 value=5 style="width:80px;">
   </div>

   <div class="container mt-5">
      <button class="btn btn-primary" id="startcount" onclick="startShowing();">カウント開始</button>
      <button class="btn btn-primary" id="endcount" onclick="stopShowing();">カウント停止</button>
   </div>
</div>

<script>
var msg;
var number;
var count = 0;
var timer_id;

function startShowing() {
   count = 0;
   number = document.getElementById("number").value;
   document.getElementById("startcount").disabled = true;
   countup();
}

function stopShowing() {
   count = 0;
   clearTimeout(timer_id);
   document.getElementById("startcount").disabled = false;
}

function countup() {
   count ++;
   document.getElementById("PassageArea").innerHTML = count;
   console.log(count % number);
   if(count % number == 0){
      sound1();
   }else{
      sound2();
   }
   timer_id = setTimeout(countup,1000);
}

function sound1(){
    var id = 'sound-file1';
    if( typeof( document.getElementById( id ).currentTime ) != 'undefined' ){
        document.getElementById( id ).currentTime = 0;
    }
    document.getElementById( id ).play() ;
}

function sound2(){
    var id = 'sound-file2';
    if( typeof( document.getElementById( id ).currentTime ) != 'undefined' ){
        document.getElementById( id ).currentTime = 0;
    }
    document.getElementById( id ).play() ;
}
</script>

<audio id="sound-file1" preload="auto">
    <source src="beep1.mp3" type="audio/mp3">
</audio>

<audio id="sound-file2" preload="auto">
    <source src="beep2.mp3" type="audio/mp3">
</audio>

</body>
</html>

動作ページはコチラ
Gitはコチラ

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

【JS】複数の二次元配列から、重複していない配列を抽出【GAS】

要旨

例: 2つの二次元配列から、重複していない配列を取得
GASで使う かもしれない

  const ary01 = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12] ];
  const ary02 = [ [1, 2, 3], [100, 99, 55], [4, 5, 6], [7, 8, 9] ];

  // [10, 11, 12],[100, 99, 55]だけ取りてえ!
  const output = [...ary01, ...ary02].filter((value) => 
    !ary01.some(ar => JSON.stringify(ar) === JSON.stringify(value)) || 
    !ary02.some(ar => JSON.stringify(ar) === JSON.stringify(value))
  );

  console.log(output);
  // [[10, 11, 12], [100, 99, 55]]

備考:配列の比較,JSON.stringify()とString()

[1,2,3] === [1,2,3]
// false
// js 配列 比較 で検索してね!   

JSON.stringify([1,2,3])
// "[1,2,3]"

String([1,2,3])
// "1,2,3"

GAS余談

Spreadsheetにて、onEdit()やonChange()は、IMPORTRANGEしているシートについては、IMPORTRANGEの元シートに編集or変更があっても発動しないようです。
IMPORTRANGEしているシートはA1とかA2あたりにIMPORTRANGE関数書いてあるだけだからね…発動したらたまったもんじゃないですね…

参考

JavaScript:filter()を使って配列内の重複要素を削除・取得したり、2つの配列から共通要素を取得する方法
配列同士で重複する値があるか確認する
2次元配列中の重複削除

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

[Rails]フォームのすべての値をワンクリックで初期化する(helperメソッド定義+JavaScript)

はじめに

様々な検索条件を付けられる検索フォームを実装している中で、検索条件をワンクリックでリセットできる方法がないかと試行錯誤しました。

結果、下記の方法で実装できたのでまとめます。

環境

  • Ruby2.5.1
  • Rails5.2.4

手順

概要を簡単に説明すると、
1. ヘルパーメソッドにリセットボタンタグを生成するメソッドを定義
2. ビューでそれを呼び出す
3. チェックボックスをjsでクリアする処理を書く
という3本でお送りする感じです

1. ヘルパーメソッドの定義

どのファイルでもいいですが、今回はhelpers/application.rbに定義します。

helpers/application.rb
module ApplicationHelper
  def reset_tag(value = "Reset form", options = {})
    options = options.stringify_keys
    tag :input, { type: "reset", value: value }.update(options)
  end
end

2. ビューファイルで呼び出し

search.html.haml
%div
  = reset_tag 'クリア', id: 'js_clear_btn'
%div
  = f.submit '完了'

本来Railsにreset_tagはありませんが、ヘルパーメソッドで定義したので、この書き方で呼び出せます。

CSSもで調整してこんな感じに↓
スクリーンショット 2020-06-03 13.53.06.png

3. チェックボックスをJavaScriptでクリアする記述

私の場合は、リセットボタンだとチェックボックスをクリア(チェックを外す)ことができなかったので、そこはJavaScript書きました。

コードは環境に大きく依存してしまうので、割愛します。

結果

f923d6ccbb11ae2bb0c6d320371bc226.gif
こんな感じで、text_fieldもnumber_fieldも、selectもcheckboxもすべて初期化するボタンを作成できました!

参考

フォームを初期化するボタンをRailsで使う

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

Angular.jsでタスク管理用スクリプトを作る

Angular.jsでタスク管理スクリプト

時間があったのでJavascriptとAngular.jsを練習

前提

  • ツールが概ねダウンロードできない(sublimeすら入らない)
  • 作業報告が主なタスク

機能

  • タスク追加
  • タスク削除
  • タスク更新
  • フォーマット化して確認
  • 日付の自動表示

画面のイメージ

実際の画面は下記です。上部の「編集」「確認」ボタンで画面が切り替わります。

編集画面

edit.png

確認画面

show.png

ソースコード

ダウンロードボタンは実装検討中(置物)です。
本当はボタンを押すだけで設定ファイルが更新されるようにしたい。

HTML

nippo.html
<!doctype html>
<html ng-app="myNippo">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
<script src="./defaultSettings.js"></script>
<script src="./nippo.js"></script>
<link rel="stylesheet" href="nippo.css">
</head>
<body>
  <div ng-controller="myController as myCtrl">
    <button class="btn primary-btn" ng-click="isEdit = true">編集</button>
    <button class="btn primary-btn" ng-click="isEdit = false">確認</button>
    <hr/>
    <div ng-if="isEdit">
      <input ng-model="startTime" ng-change="startChange()" />
      <input ng-model="endTime" ng-change="endChange()" />
      <div>
        <span>勤務時間: {{workingHours}}H</span>
        <span ng-if="workingMinutes">{{workingMinutes}}m</span>
      </div>
      <div>作業時間: {{executionHour}}<span ng-if="executionMinute && executionMinute == 30">.5</span>H<span ng-if="executionMinute && executionMinute != 30">{{executionMinute}}m</span>
      </div>
      <hr/>
      <div ng-repeat="tValue in taskValues">
        <div ng-if="myCtrl.hasTask(tValue)">
          {{tValue}}
          <div ng-repeat="task in tasks">
            <div ng-if="task.value==tValue">
              <input type="checkbox" ng-model="task.checked">
               <input type="text" class="name-input" ng-model="task.name">
               <input type="number" class="time-input" ng-model="task.hour" ng-change="myCtrl.calcHoursFromTasks()">H
               <span ng-if="task.minute"><input type="number" class="time-input" ng-model="task.minute" ng-change="myCtrl.calcHoursFromTasks()">m</span>
            </div>
          </div>
        </div>
      </div>
      <button class="btn alert-btn" ng-click="myCtrl.deleteTasks()">削除</button>
      <div class="block">
        <input ng-model="myCtrl.addTaskName" type="text" placeholder="タスク">
        <input ng-model="myCtrl.addTaskHour" type="number" placeholder="時" class="time-input">H
        <input ng-model="myCtrl.addTaskMinute" type="number" placeholder="分" class="time-input">m
        <select ng-model="myCtrl.addTaskValue"  ng-options="value for value in taskValues" ng-required="true"></select>
        <div>
          <textarea ng-model="myCtrl.addTaskText"></textarea>
        </div>
        <button class="btn primary-btn" ng-click="myCtrl.onAdd()" >追加</button>
      </div>
      <div>
        <hr/>
        <button class="btn normal-btn" ng-click="myCtrl.downloadTasks()">ダウンロード</button>
        <button class="btn normal-btn" ng-click="displayTasks = !displayTasks">defaultSettings表示切替</button>
        <div ng-if="displayTasks">
          <pre>
var defaultTasks = {{tasks|json}};
var defaultStartTime="{{ startTime }}";
var defaultEndTime="{{ endTime }}";
var defaultTaskValues={{taskValues}};
          </pre>
        </div>
      </div>
    </div>
    <div ng-if="!isEdit">
      <label class="normal-label">日付: </label>
      {{current | date: 'yyyy/MM/dd'}}<br/>
      <hr/>
      <br/>
      〇本日の作業<br/>
      <br/>
      トータル時間:{{executionHour}}<span ng-if="executionMinute && executionMinute == 30">.5</span>H<span ng-if="executionMinute && executionMinute != 30">{{executionMinute}}m</span><br/>
      <br/>
      <div ng-repeat="tValue in taskValues">
        <div ng-if="myCtrl.hasTask(tValue)">
          {{tValue}}
          <div ng-repeat="task in tasks">
            <div ng-if="task.value==tValue">・{{task.name}}: {{task.hour}}<span ng-if="task.minute && task.minute == 30">.5</span>H<span ng-if="task.minute && task.minute != 30">{{ task.minute }}m</span>
              <div ng-if="task.text" class="task-text-area">{{task.text}}
              </div>
            </div>
          </div>
          <br/>
        </div>
      </div>
    </div>
  </div>
</body>
</html>

Javascript

nippo.js
let app = angular.module('myNippo', []);
app.controller('myController', function($scope) {
    const restHour = 1;
    $scope.startTime = defaultStartTime;
    $scope.endTime = defaultEndTime;
    $scope.current = new Date();

    $scope.isEdit = true;
    $scope.displayTasks = false;

    $scope.taskValues = defaultTaskValues;
    $scope.tasks = defaultTasks;


    // 就業時間から勤務時間計算
    let calcHoursFromStoE = function(startTime, endTime) {
      $scope.workingHours = 0;
      $scope.workingMinutes = 0;
      let startTimeArray = startTime.split(':');
      const startHour = startTimeArray[0];
      const startMinute = startTimeArray[1];
      let endTimeArray = endTime.split(':');
      const endHour = endTimeArray[0];
      const endMinute = endTimeArray[1];

      let workingHour = endHour - startHour - restHour;
      let workingMinute = endMinute - startMinute;
      if (workingMinute == 30) {
        $scope.workingHours = workingHour + 0.5;
      } else if (workingMinute < 0) {
        workingHour--;
        let resultMinute = (60 - parseInt(startMinute)) + parseInt(endMinute);
        if (resultMinute == 30) {
          $scope.workingHours = workingHour - 1 + 0.5;
          $scope.workingMinutes = null;
          return;
        }
        $scope.workingHours = workingHour;
        $scope.workingMinutes = resultMinute;
      } else{
        $scope.workingHours = workingHour;
        $scope.workingMinutes = workingMinute;
      }
    }

    // タスクから勤務時間計算
    this.calcHoursFromTasks = function() {
      let hours = 0;
      let minute = 0;
      $scope.tasks.forEach(task => {
        if (task.hour != null) {
          hours = hours + parseFloat(task.hour);
        }
        if (task.minute != null) {
          minute = minute + parseInt(task.minute);
        }
        if (minute >= 60){
          hours++;
          minute = minute - 60;
        }
      });
      $scope.executionHour = hours;
      $scope.executionMinute = minute;
    }

    calcHoursFromStoE($scope.startTime, $scope.endTime);
    this.calcHoursFromTasks();

    // 開始時間変更
    $scope.startChange = function() {
      calcHoursFromStoE($scope.startTime, $scope.endTime);
    }

    // 終了時間変更
    $scope.endChange = function() {
      calcHoursFromStoE($scope.startTime, $scope.endTime);
    }

    // タスク追加
    this.onAdd = function() {
        // 時刻が空で分のみ入力されたとき、時刻に0を入力する
        if (this.addTaskMinute != null && this.addTaskHour == null) {
          this.addTaskHour = 0;
        }
        if (this.addTaskValue == null) {
          alert('タスク種別が選択されていません');
          return;
        }

        $scope.tasks.push({ name: this.addTaskName, hour: this.addTaskHour, minute: this.addTaskMinute, value: this.addTaskValue, text: this.addTaskText });
        this.addTaskName = "";
        this.addTaskHour = null;
        this.addTaskMinute = null;
        this.addTaskValue = null;
        this.addTaskText = "";

        this.calcHoursFromTasks();
    }

    // タスク削除
    this.deleteTasks = function() {
        for (let i = 0;i < $scope.tasks.length; i++) {
            if ($scope.tasks[i].checked) {
               $scope.tasks.splice(i--, 1);
            }
        }
    }

    // defaultTasksダウンロード(TBD: 実装方法検討中)
    this.downloadTasks = function() {
        let array = ["let defaultTasks = " + JSON.stringify($scope.tasks)]
        let blob = new Blob(array ,{type:"text/json"});
        let link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.download = 'defaultSettings.txt';
        link.click();
    }

    this.hasTask = function(value) {
        for (let i = 0; i < $scope.tasks.length; i++){
            if ($scope.tasks[i].value == value) {
                return true;
            }
        }
        return false;
    }
});

CSS

気持ちばかりのCSS

nippo.css
.time-input {
    width: 50px;
}

.name-input {
    width: 200px;
}

.task-text-area {
    white-space: pre-line;
}

.block {
    padding: 10px 0px 10px 5px;
}

.btn {
    text-align: center;
    margin: 5px;
    padding: 2px 7px 2px 7px;
    border: 1px solid transparent;
    border-radius: .25rem;
}

.normal-btn {
    color: white;
    background-color: lightseagreen;
    border-color: lightseagreen;
}

.primary-btn {
    color: white;
    background-color: cornflowerblue;
    border-color: cornflowerblue;
}

.alert-btn {
    color: white;
    background-color: crimson;
    border-color: crimson;
}

.normal-label {
    color: white;
    background-color: lightseagreen;
    text-align: center;
    padding: 2px 7px 2px 7px;
    border-radius: .25em;
    font-weight: bold;
    font-size: small;
}

設定用ファイル

defaultSettings.js
var defaultTasks = [
  {
    "name": "リファクタリング",
    "hour": 0.5,
    "value": "【開発】"
  },
  {
    "name": "メールチェック",
    "hour": 0.5,
    "value": "【その他】"
  }
];
var defaultStartTime="9:00";
var defaultEndTime="17:30";
var defaultTaskValues=["【開発】","【その他】"];

できるならコンポーネント化したりしたいところ。
script指定するだけで使えるのは助かりました。

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

[備忘メモ]JavaScript の Date を「YYYY-MM-DD」の String に変換する関数

vuejs-datepickerでカレンダーから日付を選択するコンポーネントを作っていたのですが、選択した月が1ヶ月ずれるという事象に出会いまして。。
結局、その問題は解決したんですが、JS の Date に関する、わかりづらい仕様を覚えておくためのメモです。

Date を String に変換する関数

作成していたカレンダーのコンポーネントでは、得られた日付(Date)を「YYYY-MM-DD」形式の文字列(String)に変換する処理を書いていたのですが、その関数が以下になります。

        init() {
            const now = new Date(Date.now());
            formatDate(now);
        },

        formatDate(val) {
            const year = val.getFullYear();
            const month = val.getMonth() + 1;
            const date = val.getDate();
            const formatDate = String(year) + '-' + String(month) + '-' + String(date);
            return formatDate;
        }

formatDate のところが実際の関数です。

const month = val.getMonth() + 1;

気をつけないといけないのは上記の部分。

なんとJSのgetMonth() が得られる月は「0~11」の間です。
なので、+ 1 しないと 1 ヶ月ずれてしまいます。

それにしてもこんな罠があるなんて。。

参考文献

https://qiita.com/labocho/items/5fbaa0491b67221419b4

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

ReactNativeで指定した文字全てを置換する方法

個人用にメモ
置換メソッドreplaceについて記述してみる

使い方はこんな感じらしい

[配列名].replace(/`変換対象の文字`/,` 変換後の文字`)

配列に入ってる,だけ取り除いてみる。

//配列生成
const str = "apple,banana,orange";
// 「,」を全て取り除く
const replaced = str.replace(/,/g, ' ')

//出力結果
console.log(replaced) 
apple banana orange

オンライン実行環境
https://paiza.io/projects/WIGdDzgEKj0Ctwf470BSBw

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

iOS,Android input="date" JavaScriptイベント取得まとめ

はじめに

Javascriptでinput="date"のイベント取得をiOS,Androidで調べたのでまとめ

検証環境

iOS 13.3
ブラウザ Safari

Android 8.0.0
ブラウザ Chrome

iOS

操作 イベント
フォーカス onchange
日付切り替え onchange
消去 onchange
完了 onblur

完了を押下するとフォーカスが外れるため'onblur'が発火するっぽい

Android

操作 イベント
フォーカス -
日付切り替え -
削除 onchange (日付が入力済みの場合)
キャンセル -
設定 onchange

比較

ios Android
フォーカス onchange -
日付切り替え onchange -
消去・削除 onchange onchange
キャンセル - -
完了・設定 onblur onchange

sample

alertだとダイアログを消した際に、blurが発生し続ける見たいだったので、HTML出力

See the Pen sample date by shotaabe (@shotaabe) on CodePen.

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

Notification API + Laravel + Vue.js、 PWAで メッセージ機能を作る 作例編

概要

以前の 製作事例公開内容となりますが
会員メンバー間で、メッセージを送受信できる機能を
Laravel+ Vue でPWA対応、実装しました。
新着の自動更新は、JSタイマで起動し、
Notification API での通知等の仕組みとなります。

参考のコード / GitHub

https://github.com/kuc-arc-f/lara58a_7message

構成

Progressive Web Apps / PWA
Notification API
Laravel 5.8
Vue.js
javascript
nginx
mysql

migrations

https://github.com/kuc-arc-f/lara58a_7message/blob/master/database/migrations/2020_05_16_155945_create_messages_table.php

画像

・通知の画面
ss-msg-notification.png

・受信一覧、
 メールのような、受信、送信タブで切替表示としました

ss-message-receive-0524.png

実装など

・JSタイマー
 定期実行、自分宛の送信メッセージを監視し、
 新着があれば、トリガー発火して。新着通知等の処理を実行します。

 function set_time_text(){
    var data = {
                'user_id': USER_ID,
                'type': 1,
            };           
    axios.post('/api-1234' , data).then(res =>  {
        var item = res.data
        if(item.id != null){
            $("input#time_text").val( item.id );
            $("input#message_title").val( item.title );
        }else{
            $("input#time_text").val( 0 );
        }
console.log( item );
    });  
 }
 set_time_text();
var timer_func = function(){
     set_time_text();
};
var TIMER_SEC = 1000 * 600;
setInterval(timer_func, TIMER_SEC );

・新着の通知

function display_notification(title, body ){
    if (!('Notification' in window)) {//対応してない場合
        alert('未対応のブラウザです');
    }
    else {
        // 許可を求める
        Notification.requestPermission()
        .then((permission) => {
            if (permission === 'granted') {// 許可
                var options ={
                    body: body,
                    icon: 'https://hoge.net/icon.png',                                      
                    tag: ''
                };
                var n = new Notification(title,options);
                console.log(n);
                setTimeout(n.close.bind(n), 5000);
            }
            else if (permission == 'denied') {// 拒否
            }
            else if (permission == 'default') {// 無視
            }
        });
    }  
}


参考のページ

https://knaka0209.hatenablog.com/entry/lara58_26message

.

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

【5分で理解】JavaScriptのユーザー定義関数について

こんにちは!
本日はユーザー定義関数について解説していきたいと思います。
JavaScriptの基本を学びたい初学者の方は是非読んでいってください!

? 関数のイメージ

関数ってよく聞くけど数学苦手マンだからよくわからん!というような私みたいな方もいると思うので、
最初に関数のイメージについてお話します。

関数を英語にすると function となります。
functionだけだとピンときませんが、意味を調べると関数の他に機能などの意味があります。

関数のフローは自動販売機によく例えられます。

           ジュースのボタンを押す
お金を入れる→自販機.png→選んだジュースが出てくる

コレを関数に比喩すると
お金を入れる  = 引数
ボタンを押す  = 処理の実行
ジュースが出る = 戻り値

というような形になります。
とりあえず、関数は自動販売機みたいな流れで動くというイメージだけ掴んでいただければOKです。

? ユーザー定義関数って何?

関数は主に2種類あります。

  • 組み込み関数 → 既にプログラムで準備されている関数
  • ユーザー定義関数 → 自身で自由に作れる関数

その名の通り、ユーザーが定義した関数のことをユーザー定義関数と呼びます。

基本的な形としては

function test(引数){
 //処理


return 戻り値; //戻り値
}

testという自販機に対して、引数を入れて処理を行い、戻り値(return)が帰ってきます。
実際には引数がなかったり、引数が複数あったりするパターンもありますので、そちらを解説していこうと思います。

? 関数のパターン

上記で書いたとおり関数の記載には複数のパターンが存在します。
何パターンか紹介していこうかと思います。

パターン1 引数なし、戻り値なし

function test(){         //引数は無いので()の中には何も書かない
 console.log('test');    //戻り地も無いのでreturnを書かない
}

test();

コレで実行すると、コンソール上に test と表示されます。
シンプルにconsole.logだけが動いている状態です

パターン2 引数あり、戻り値なし

const text = '春日部防衛隊ファイヤー!!!'
const text_2 = '救いのヒーローぶりぶりざえもん参上'

function getText(string){    //stringを引数に
 console.log(string);        //ここの()の中身は必ず引数と同じ名前にする必要がある。
}

getText(text);             //勘違いしやすいですが、ここの()の中身は引数と同じにする必要がありません。

コレを実行すると、春日部防衛隊ファイヤー!!!と出力されます。
このように、変数を代入して使うことが多いです。
またgetText(text_2);とすることで、救いのヒーローぶりぶりざえもん参上と出力されます。

パターン3 引数なし、戻り値あり

このパターンは少し複雑になります。
下記のパターンで処理を実行した場合、どうなるでしょうか?

function textNumber(){
 return 5 ;
}
textNumber();

一見するとreturnの5が、コンソール上に反ってきそうにも見えますが、
このコードに出力する処理が書かれていないので、何も表示はされません。
しかし、処理自体は行われているのでtextNumberに5という数字自体は入っています。

function textNumber(){
 return 5 ;
}
console.log(textNumber());

このようにconsole.logでかこってあげると5と出力できます。
あるいは

function textNumber(){
 return 5 ;
}
textNumber();                     //この段階で5がtextNumberに入ってる。

const TextNumber = textNumber();   //textNumberを変数TextNumberに代入する

console.log(TextNumber)           //変数を出力する

このように変数化して出力することも可能です!
これでも5が出力されます。

パターン4 インプット2つ、戻り値あり

今回は足し算の合計を出力するコードを作ります。

function sumNumber(int1,int2){   //int1=4,int2=6が代入される。下記の第一引数、第二引数がそれぞれint1,int2似自動で振り分けられる。
 let int3 = int1 + int2;         
 return int3;
}
const total = sumNumber(4,6);  //第一引数に4を第二引数に6を入れる

console.log(total);

この出力は、10となるのがわかるかと思います。
ポイントは引数と処理の中の文字をあわせることに注意するくらいだと思います。

非常に簡単でしたが、ユーザー定義関数について簡単にパターンと使い方について解説していきました。
今回は以上となります!

? 次回予告

次回は、組み込み関数についても解説できたらなと思います。

私事ですが、今後はAWS認定ソリューションアーキテクト アソシエイトに挑戦する予定ですので更新頻度が下がるか、備忘録的にそちらの記事を書いていくかと思われます。

記事を読んでいただきありがとうございました。
間違えている部分などありましたらご指摘のほどお願い致します

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

GAS:リストをもとに、テンプレートシートを複製し、シート名をリネームする

qiita arrayPractice.js
const ThisSpread = SpreadsheetApp.getActiveSpreadsheet();

function createSheet(){
  const name_list = createNameList();
  Logger.log(name_list);
  const TemplateSheet = ThisSpread.getSheetByName('template');
  for (let name of name_list){
  const create_sheet = TemplateSheet.copyTo(ThisSpread);
  create_sheet.setName(name);
  }
}
function createNameList() {
  const name_list = [];
  const NamesSheet = ThisSpread.getSheetByName('names');
  for (row = 1; row <= 5; row ++){
  name_list.push(NamesSheet.getRange(row, 1).getValue());
  }
  //Logger.log(name_list);
  return name_list;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.js で npm モジュールをプラグイン化するサンプル(IMI コンポーネントツール)

概要

  • Nuxt.js で npm モジュールをプラグイン化して使う例をメモとして残す
  • ついでに最近リリースされた IMI コンポーネントツールを使ってみる

手順

npm モジュール追加

yarn add https://info.gbiz.go.jp/tools/imi_tools/resource/imi-enrichment-contact/imi-enrichment-contact-1.0.0.tgz

プラグイン作成

  • plugins 以下に javascript ファイルを作成
  • モジュールを import してコンテキストと Vue コンポーネントにインジェクトする
plugins/contact.js
import contact from 'imi-enrichment-contact';

export default (ctx, inject) => {
  ctx.$contact = contact;
  inject('contact', contact);
};

nuxt.config.js を修正してロード

nuxt.config.js
 plugins: ['@/plugins/contact'],

.vue ファイルから使用する

xxx.vue
  asyncData({ $contact }) {
    console.log($contact('117'));
  },
  methods: {
    normalize() {
      this.normalized = JSON.stringify(this.$contact(this.tel), null, 4);
    }
  }

動作サンプル

補足

  • IMI コンポーネントツールはブラウザで動かないものもある(住所とか)
  • プラグインを TypeScript から使う場合は型定義なども別途必要

参考リンク

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

GAS:javaScriptでリスト化されたタイトルを各シートのrange(1,1)にsetValue()する

環境

GAS

前提

criteriaシートのA1からB4まで、シート名とタイトルが記載されています。

qiita arrayPractice.js
const this_spread = SpreadsheetApp.getActiveSpreadsheet();

function Main() {
  const name_map = createMap();
  const error_list = [];
  for (let name in name_map){
    //Logger.log(name);
    //Logger.log(name_map[name]);
    try{
    const target_sheet = this_spread.getSheetByName(name);
    Logger.log(name + '' + name_map[name]);
    target_sheet.getRange(1,1).setValue(name + '' + name_map[name]); 
    }catch(error){
    error_list.push(name);
    }
  }
  Browser.msgBox('error_list is ' + error_list);
}

function createMap() {
  const criteria_sheet = this_spread.getSheetByName('criteria');
  let name_map = new Map();
  for(let count = 1; count <= 4; count ++){
  name_map[criteria_sheet.getRange(count,1).getValue()] = criteria_sheet.getRange(count,2).getValue();
  }
  return name_map
}

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

【Vue.js】半日でできる、ビデオチャット上で動くオリジナルゲームの開発から公開まで。

Vue.jsを使って簡単なゲーム開発し、完成したものをwh.imというゲームのプラットフォームに公開するまでをまとめてみました。
この記事を読めば、Vue.js未経験の方であっても数時間あればオリジナルゲームを開発・公開し、友達と遊べるようになります!
できる限りわかりやすく書いたので長くなりましたが、ぜひ最後まで読んでみてください(記事の完成に3日かかりました? 服装の変化を楽しんでください)。ご質問はコメントまで!

対象読者

  • html / css はある程度わかる方
  • Vue.jsに入門したい人
  • ビデオチャット上でゲームを作ってみたい方

はじめに

wh.im というサービスの開発している@aitaroです。Vue.jsを使い始めてかれこれ1年半になります。今でこそ wh.im を開発できるぐらいの知識がつきましたが(wh.imはVue.jsとそのフレームワークであるNuxt.jsを使っています)、Vue.jsを始めたときは右も左もわからない状態でした。そこで、同じようにVue.js初心者の方に対して、ゲームの作成といった一つのプロダクトの完成とデプロイを目指した入門記事を今回書こうと思いました。wh.im要素が多めはなりますが、Vue.jsの基礎も理解できるようになっているので、是非最後まで走りきってみてください。

また、Vue.js経験者の方にも、wh.imの始め方から公開の仕方までが一通りまとまっているので、ビデオチャットで遊べるでゲームを作ってみたいときは是非参考にしてください。

構成

このエントリは以下の6つのパートと21の章から成ります。

Ⅰ. wh.imとは?

Ⅱ. Vue.jsの環境構築

  • 1. node.jsをインストール
  • 2. npmでvue-cliをインストール
  • 3. Vue.jsのプロジェクトを作る
  • 4. ソースコードを変更してみる。

Ⅲ. wh.imでの開発のための環境設定

  • 5. wh.im上でvueを動かしてみる
  • 6. wh.im上で開発の準備

Ⅳ. ゲームの実装

  • 7. main.jsのでライブラリ読み込み
  • 8. vue.jsのコンポーネントの追加
  • 9. じゃんけん画像の設置
  • 10. グーを選択するメソッドを追加しよう!
  • 11. 選んだ手のstateの登録
  • 12. チョキやパーを選択できるようにしよう!
  • 13. 関数を抽象化しよう!
  • 14. 選択後の画面を作ろう
  • 15. 結果の画面を作ろう
  • 16. 結果の画面に全員の選んだ手を表示してみよう!
  • 17. 最後に見た目を整えよう!

Ⅴ デプロイして公開する

  • 18. githubにコードを上げる
  • 19. netlifyの設定をする
  • 20. wh.imのdevelop画面でゲームの登録をする
  • 21. 遊んで見る!

Ⅵ. まとめ

wh.imとは?

wh.imとは、ビデオチャットしながら遊べるゲームのプラットフォームです。wh.imにアクセスすると、じゃんけんやワードウルフなどのゲームが友達と遊べます。さらに、wh.imの特徴はなんと言ってもオリジナルゲームを投稿できること!自分で作ったゲームを登録すれば、今日中にでもそのゲームで友達と遊ぶことができます。今回はこの wh.im を題材に使って、Vue.jsを用いたゲームを作っていこうと思います!
image.png

wh.imについて詳しくしりたい方は、新型コロナの自宅待機中に、ビデオチャットしながらゲームで遊べるサービスを作った話を是非読んでみてください。

Vue.jsの環境構築

1. node.jsをインストール

まずはMacにnode.jsをインストールしてください。インストールの方法はこちらの記事に簡潔にまとまっています。
Macにnode.jsをインストール

2. npmでvue-cliをインストール

node.jsをインストールしたあなたは、npm使えるようになっているはずです。npmとは、node package managerの略で、JavaScript系のパッケージを管理するためのツールです。
以下の方法で、npmを使ってvue-cliをインストールしましょう。
-gをつけることで、グローバルにインストールされるので、どこからでもvueコマンドを呼び出せるようになります。

$ npm install -g @vue/cli

3. Vue.jsのプロジェクトを作る

ここから、簡単なじゃんけんアプリの開発を通じてVue.jsを使ったゲーム開発を学んでいきましょう。

まずはVue.jsのプロジェクトを作成してみます。今回はじゃんけんゲームを作っていくので、jankenとしますが、好きな名前で大丈夫です。

$ vue create janken

すると、次のような選択画面になります。

? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint)
  Manually select features

これはそのままEnterを押してしまいましょう。
最後にこのような画面ができたら成功です!

?  Successfully created project janken.
?  Get started with the following commands:

 $ cd jaknen
 $ npm run serve

作成されたら、画面の指示に従ってそのjankenというディレクトリに移動してみましょう。
この段階で試しに起動してみましょう。

$ cd janken
$ npm run serve

数秒でターミナルがこのような状態になったら成功です。

起動は成功しているので、Chrome等ブラウザのアドレスバーに
http://localhost:8080
と入力してみてください。

このような画面が出てくると思います。
これで環境構築はひとまず終了です!お疲れ様でした?

4. ソースコードを変更してみる。

これで、Vue.jsの起動は完了しました。
Vue.jsを初めて触る人向けに、コードの雰囲気を掴んでもらいます。
まず、vue create で作成されたサンプル画面を見ていきましょう。
このサンプル画面のコードは src > components > HelloWorld.vue にあります。

ではそのうちこの文章を変えてみましょう。

src/components/HelloWorld.vue
    <p>
      For a guide and recipes on how to configure / customize this project,<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>

これを適当に変えてみます。

src/components/HelloWorld.vue
    <p>
      100日目にVue.jsをマスターする俺<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>

変えたら、command + S(winはctrl + S)で変更を保存してみましょう。
すると、ブラウザでみると自動で反映されていると思います!

これはVue.jsのうちのWebpackというライブラリで、HotReloadという機能です!変更が保存されると、自動で反映されます!ただ時々バグるので、もしバグったなというときはおとなしくブラウザの画面をリロードしましょう。

wh.imでの開発のための環境設定

5. wh.im上でvueを動かしてみる

wh.im上でVueを動かしてみましょう。
wh.imから遊び場を作成します。

次に、wh.imを開発モードにします。
URLの最後に&develop=trueを追加して、リロードすると、wh.imが開発モードに変わります。

4開発モードになると、右上のボタンからアプリを選択するときに、開発用(port:8080)が出てきます。

この開発用(port:8080)を選択し、プレイすると先程の画面になります。

これで、開発の準備は整いました。

6. wh.im上で開発の準備

では、実際にwh.imを使ったゲームを作っていきましょう。wh.imはVueで開発しやすいように、ライブラリを用意しています。(ライブラリとは拡張機能みたいなもので、追加することでVueでやれることが広がります)
一旦、ターミナルにいき、 command + C (windows の場合は ctrl + C)でサーバーを止めましょう。
その後、次のコマンドを打ち込んでライブラリを追加します。

$ npm install whim-client-vue

これで、ライブラリが入りました!
package.jsonを開いてみると、追加されているのがわかります。(バージョンは多少ことなる場合があります。)

package.json
{
  "name": "janken",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "whim-client-vue": "^1.1.4" // ←ここ
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.4.0",
    "@vue/cli-plugin-eslint": "~4.4.0",
    "@vue/cli-plugin-vuex": "~4.4.0",
    "@vue/cli-service": "~4.4.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "vue-template-compiler": "^2.6.11"
  }
}

そして次に、Vueの設定を変更します。janken(ルートディレクトリ)の直下にvue.config.jsを作り、以下のコードを加えましょう。
これはクロスドメインでアプリを呼び出すときに、host名を明示的にlocalhostにするためです。

vue.config.js
module.exports = {
  devServer: {
    host: "localhost"
  },
};

これでもう一度、Vueを起動します。

$ npm run serve

vueを再起動したときは、wh.imの方でも一回、ゲームを終了して再度選択する必要があります。

ゲームの実装

7. main.jsのでライブラリ読み込み

ここからは実際にコードを書いていきます。
まず、src/main.jsを開き、whim-client-vueを使うように設定します。
3,4行目を増やしました。

src/main.js
import Vue from 'vue'
import App from './App.vue'
import whimClientVue from "whim-client-vue";
Vue.use(whimClientVue);

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

これは、whim-client-vueのライブラリを呼び出すコードです。

src/main.js
import whimClientVue from "whim-client-vue";

Vue.useを使うことで、さきほど呼び出した、whim-client-vueのライブラリを、Vueに登録します。

src/main.js
Vue.use(whimClientVue);

8. vue.jsのコンポーネントの追加

次に、App.vueを編集します。これは、ページにアクセスすると最初に表示される画面です。
これをデフォルト画面から変更してみます。変更点は、divタグの中身を消したこと、HelloWorldコンポーネントは今回使わないので、componentsを消したことです。

src/App.vue
<template>
  <div id="app">
    Player: {{ $whim.accessUser.name }}
  </div>
</template>

<script>
export default {
  name: "App",
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

すると、自分の名前が表示されていると思います。
template内の {{ }} は、そこの部分を式評価するという意味です。
今回は、そこに $whim.accessUser.name と記述することで、アクセスしてるユーザーの名前がとれました。
ここで言う、アクセスしているユーザーとはそのゲームをプレイしている人のことです。wh.imはビデオチャットサービスなので、複数人でゲームをプレイします。そのとき、このブラウザからアクセスしている人はだれかということをゲーム側で把握しなければなりません。
イメージ図

実際に確認してみましょう。プライベートブラウザや、違うブラウザで、wh.imの同じルームにアクセスします。すると今度は、 Player:モナリザ と表示されました。

このようにアクセスする人によって、表示を変えたい場合は、accessUserを使うことで可能になります。

9. じゃんけん画像の設置

次に、ゲーム上にじゃんけんの選択肢を配置しましょう。
まず、じゃんけんの画像を用意します。
私は素材ライブラリーさんの画像を使わせていただきました。

ダウンロードした画像を、 src/assetsの中に配置します。
ファイル構成は以下のようになると思います。

janken
└── assets
    ├── paper.png
    ├── rock.png
    └── scissors.png

そしてこれを先程のApp.vue画面に配置します。
先程の、Player: {{ $whim.accessPlayer.name }}を下のように書き換えてください。

src/App.vue
    <div>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
        />
      </div>
    </div>

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

そして、wh.imの画面を確認します。すると、以下のような選択画面が現れていると思います!

10. グーを選択するメソッドを追加しよう!

次に、グーチョキパーを選択できるようにします。
目標は、画像をクリックすると文字が選択済み!に変わって、データベースに選択した手が書き込まれることです。
ではまず、データベースに書き込む処理をしましょう。

グーから行きます。
グーの画像に@click="selectRock"を付け足します。以下のような感じです。これはclickしたときにselectRockという関数を実行するという意味です。

src/App.vue
<img
  src="@/assets/rock.png"
  width="150"
  height="150"
  @click="selectRock"
/>

そしてselectRock関数をvueに登録します。
を以下のように編集します。

src/App.vue
<script>
export default {
  name: 'App',
  methods: {
    selectRock() {
      // ここに処理を書いていく
      console.log('selectされた!')
    }
  }
}
</script>

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"  
          @click="selectRock"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
    },
  },
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

ここで出てきたconsole.logはブラウザのconsole画面にログを表示する命令です。
これで、グーを押したときの挙動を確認しましょう。
wh.imの画面に戻って、Chromeのデベロッパーコンソールを開きます。macの方は command + option + i で開きます。 そして、その状態でグーをクリックします。するとコンソール画面にselectされた!と表示されるはずです。(以下の画像を参照)

このように@clickでselectRock関数が実行されたのがわかると思います。

11. 選んだ手のstateの登録

次にデータベースに登録しましょう。selectメソッドを変更していきます。
データーベースはwh.im上ではstateと呼ばれます。また、このstateはデーターベースといいつつ、いわゆるRDBではなくJSON型で保存されます。(そしてこのデーターベースはこのアプリを立ち上げているブラウザ間で同期されます。)
stateを変更するときは、$whim.assignState関数を使います。
今回はアクセスしてるユーザーがグーを出したことを保存したいので、stateに[this.$whim.accessUser.id]: "rock"を書き込みます。

src/App.vue
selectRock() {
  // ここに処理を書いていく
  console.log('selectされた!')
  this.$whim.assignState({
    [this.$whim.accessUser.id]: "rock"
  })
}

動作確認をしてみましょう。グーをクリックしてみます。するとstateにグーが登録されます。wh.imの開発モードにはこのstateを確認する機能があります。右上のメニューボタンから、SHOW APP STATE を選んでください。
すると次にような画面が出てきます。

このObjectがstateです。
今回はstateにoNyPTFZuMbOOZruCR4UMSsP9zRu2:"rock"が登録されていることがわかりました。この、oNyPTFZuMbOOZruCR4UMSsP9zRu2はユーザーidです。

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"  
          @click="selectRock"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    selectRock() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "rock"
      })
    },
  },
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

12. チョキやパーを選択できるようにしよう!

今のところまだ、グーしか選択できません。チョキやパーも選択できるようにしましょう。これは今までの要領でいくと簡単です。
まずselect関数を2こ増やします。

src/App.vue
<script>
export default {
  name: 'App',
  methods: {
    selectRock() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "rock"
      })
    },
    selectScissors() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "scissors"
      })
    },    
    selectPaper() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "paper"
      })
    }
  }
}
</script>

これで、関数が3つに増えました。これをそれぞれの画像のclickイベントに追加していきます。

src/App.vue
<div>
    <img
      src="@/assets/rock.png"
      width="150"
      height="150"
      @click="selectRock"
    />
    <img
      src="@/assets/scissors.png"
      width="150"
      height="150"
      @click="selectScissors"
    />
    <img
      src="@/assets/paper.png"
      width="150"
      height="150"
      @click="selectPaper"
    />
</div>

これで、3つともクリックしたらstateに反映されるようになりました!
(注意事項:動作確認は必ずSTATE確認画面を閉じてから行ってください。)

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"  
          @click="selectRock"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
          @click="selectScissors"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
          @click="selectPaper"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    selectRock() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "rock"
      })
    },
    selectScissors() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "scissors"
      })
    },    
    selectPaper() {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: "paper"
      })
    }
  },
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

13. 関数を抽象化しよう!

先程のようにグー・チョキ・パーselect関数を3つ作ってもいいですが、これらの関数には似たような処理が多いです。こういうときは一つの関数にまとめます。これはプログラミングにおいて重要な抽象化の概念です。処理の内容を日本語化するとわかりやすいと思います。

まとめる前

  • selectRock関数: グーをstateに登録する。
  • selectScissors関数: チョキをstateに登録する。
  • selectPaper関数: パーをstateに登録する。

まとめた後

  • select関数: 出した手をstateに登録する。

グー、チョキ、パーが出した手に変わったこと以外は同じです。けどこのままだと、出した手ってなんやねん!ってなると思います。ここで関数の引数機能を使います。引数は関数名(引数名)みたいな感じで使います。
では早速コードを書いてみましょう。

src/App.vue
<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: hand
      })
    }
  }
}
</script>

そして、この関数の呼び出し側も変更します。

src/App.vue
  <div>
    <img
      src="@/assets/rock.png"
      width="150"
      height="150"
      @click="select('rock')"
    />
    <img
      src="@/assets/scissors.png"
      width="150"
      height="150"
      @click="select('scissors')"
    />
    <img
      src="@/assets/paper.png"
      width="150"
      height="150"
      @click="select('paper')"
    />
  </div>

これで、先程と同じ用に動くはずです。確認してみましょう。

お気づきの人もいるかも知れませんが、このステップでは機能としてなにも進んでいません。コードの書き方を変えただけです。この機能を変えずによりよいコードにすることをリファクタリングといいます。「機能が増えないなら時間の無駄やんw」という人もいるかも知れませんが、どんどん大きなプロジェクトになっていくと、それをメンテナンスするためにはコードの品質が重要になっていきます。そして、「良いコード」を書くのは一朝一夕でできることでもないので、普段から意識的に「良いコード」を書くことを意識することがおすすめです。

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
          @click="select('rock')"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
          @click="select('scissors')"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
          @click="select('paper')"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: hand
      })
    },
  },
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

14. 選択後の画面を作ろう

今のままですと、何回もじゃんけんが選択できてしまいます。選択が終わったら、選択済みにかえましょう。
選択が終わったかどうかは、stateを見たらわかります。stateが空なら未選択ですし、stateに自分のaccessUserIdがあれば、選択済みです。これは$whim.state[$whim.accessUser.id]で確認できます。また、vue.jsのテンプレートではv-if, v-else を使って条件分岐ができます。確認してみましょう。

src/App.vue
<template>
  <div id="app">
    <div v-if="$whim.state[$whim.accessUser.id]">
      <h2>
        {{ $whim.state[$whim.accessUser.id] }}を選択済みです。
      </h2>
    </div>
    <div v-else>
      <h2>
        選択してください!
      </h2>
(以下略)

$whim.stateというのは以下のようなObject型です。(SHOW APP STATE で確認できると思います。)

{
    jibunNoID: "paper"
}

ここでいうjibunNoID$whim.accessUser.idで取れる値です。(これはscriptの方で書いたthis.$whim.accessUser.idと同じですが、vueのtemplateではthisは省略します。)
なので、すでにpaperを選んでいる場合は$whim.state[$whim.accessUser.id]の値はpaperになりますし、何も選んでいない場合はundefinedになります。これを利用して、v-ifで条件分岐をしています。

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div v-if="$whim.state[$whim.accessUser.id]">
      <h2>
        {{ $whim.state[$whim.accessUser.id] }}を選択済みです。
      </h2>
    </div>
    <div v-else>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
          @click="select('rock')"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
          @click="select('scissors')"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
          @click="select('paper')"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: hand
      })
    },
  },
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

15. 結果の画面を作ろう

最後に結果の画面です。まずは、二人プレイでどのようにstateが登録されているか確認します。

一人目はpaperを選んでいて、二人目はrockを選んでいます。これを画面に表示します。

まずはじめに、全員がじゃんけんの手をすでに選んだかどうかを確認します。javascriptではfor文でループを表すことができます。usersを一人ずつ、stateに手が登録されているかを確認します。for文の詳しい説明はこちらなどを参考にしてください。

src/App.vue
let result = true
for (let i = 0; i < this.$whim.users.length; i++ ) {
    if(!this.$whim.state[this.$whim.users[i].id]){
        result = false
    }  
}
return result

ここで、this.$whim.usersは今部屋にいるuser全員です。
まず、resultにtrueを設定しておきます。そして、users一人ずつ、手がセットしているかをthis.$whim.state[this.$whim.users[i].id]で確認します。
ここでセットされていなかったら、resultをfalseに変更します。こうすることで、ひとりでもじゃんけんの手を選んでなかったらresultがfalseになります。これを関数として実装し、またtemplate側でv-ifを使います。(もともと、v-ifだったところはv-else-ifに変えました)

この処理をisEveryoneSelectとして追加したコードは次のようになります。

src/App.vue
<template>
  <div id="app">
    <div v-if="isEveryoneSelect">
      <h2>
        全員が選択を終わりました。
      </h2>
    </div>
    <div v-else-if="$whim.state[$whim.accessUser.id]">
      <h2>
        {{ $whim.state[$whim.accessUser.id] }}を選択済みです。
      </h2>
    </div>
    <div v-else>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
          @click="select('rock')"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
          @click="select('scissors')"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
          @click="select('paper')"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: hand
      })
    },
  },
  computed: {
    isEveryoneSelect() {
      let result = true
      for (let i = 0; i < this.$whim.users.length; i++ ) {
        if(!this.$whim.state[this.$whim.users[i].id]){
            result = false
        }  
      }
      return result
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

ここで使用したcomputedとは、値を動的に算出するときに使うものです。今回ですと、usersが変わりうるので、methodsよりもcomputedのほうが適切です。詳しくはこの記事を読むといいでしょう。
では実際に確認しましょう。一人プレイにときはじゃんけんの手を選ぶと、すぐに全員が選択を終わりましたと出ると思いますが、2人プレイのときは、選択画面→グーを選択済みです。→全員が選択を終わりました。と順番に変更すると思います。

16. 結果の画面に全員の選んだ手を表示してみよう!

全員の選択が終わったら結果を表示します。ここではVue.jsのv-forという機能を使います。v-forはtemplate内でループをするという機能です。今回の場合、全員分の結果を表示させないと行けないので、usersに対してv-forでループさせます。

src/App.vue
<div v-if="isEveryoneSelect">
  <h2 v-for="user in $whim.users" :key="user.id">
    {{user.name}}の出した手は、{{$whim.state[user.id]}}です。
  </h2>
</div>

最終的に以下のような結果画面になれば成功です。

App.vue全体
src/App.vue
<template>
  <div id="app">
    <div v-if="isEveryoneSelect">
      <h2 v-for="user in $whim.users" :key="user.id">
        {{user.name}}の出した手は、{{$whim.state[user.id]}}です。
      </h2>
    </div>
    <div v-else-if="$whim.state[$whim.accessUser.id]">
      <h2>
        {{ $whim.state[$whim.accessUser.id] }}を選択済みです。
      </h2>
    </div>
    <div v-else>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
          @click="select('rock')"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
          @click="select('scissors')"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
          @click="select('paper')"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: hand
      })
    },
  },
  computed: {
    isEveryoneSelect() {
      let result = true
      for (let i = 0; i < this.$whim.users.length; i++ ) {
        if(!this.$whim.state[this.$whim.users[i].id]){
            result = false
        }  
      }
      return result
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

17. 最後に見た目を整えよう!

最後に画像やcssを使って、見た目を整えます。Vue.jsでは srcの前でコロンをつけることで、動的に値を設定します。

全体のコードはこちら

src/App.vue
<template>
  <div id="app">
    <!-- class="result"を追記します。 -->
    <div v-if="isEveryoneSelect"  class="result">
      <div v-for="user in $whim.users" :key="user.id">
        <!-- じゃんけんの画像(rock.pngなど)を出した手に応じて表示します。 -->
        <img
          :src="require('@/assets/' + $whim.state[user.id] + '.png')"
          width="150"
          height="150"
        />
        <h2>{{user.name}}</h2>
      </div>
    </div>
    <div v-else-if="$whim.state[$whim.accessUser.id]">
      <h2>
        {{ $whim.state[$whim.accessUser.id] }}を選択済みです。
      </h2>
    </div>
    <div v-else>
      <h2>
        選択してください!
      </h2>
      <div>
        <img
          src="@/assets/rock.png"
          width="150"
          height="150"
          @click="select('rock')"
        />
        <img
          src="@/assets/scissors.png"
          width="150"
          height="150"
          @click="select('scissors')"
        />
        <img
          src="@/assets/paper.png"
          width="150"
          height="150"
          @click="select('paper')"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    select(hand) {
      // ここに処理を書いていく
      console.log('selectされた!')
      this.$whim.assignState({
        [this.$whim.accessUser.id]: hand
      })
    },
  },
  computed: {
    isEveryoneSelect() {
      let result = true
      for (let i = 0; i < this.$whim.users.length; i++ ) {
        if(!this.$whim.state[this.$whim.users[i].id]){
            result = false
        }  
      }
      return result
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
/* resultクラスに対応するcssを追記します。 */
.result {
  display: flex;
  justify-content: center;
}
</style>

画像は、この行の$whim.state[user.id]('rock', 'cissors', 'paper'のいずれか)が動的に表示されます。

:src="require('@/assets/' + $whim.state[user.id] + '.png')"

ここまでで、実装は終わりです。お疲れ様でした?

デプロイして公開する

18. githubにコードを上げる

ではここから、このゲームの公開方法に移ろうと思います。今のところ、このゲームはローカルで動いてるだけなので、なんらかのサーバにホスティングする必要があります。まず1ステップ目として、今作ったゲームをgithubに上げましょう。具体的は方法はここらへんの記事が参考になると思います。

まずは、新しいリポジトリを作ります。

その後、codeをgithubに上げます。
以下のようなコマンドになるともいます。

$ git add .
$ git commit -m "janken完成"
$ git remote add origin git@github.com:username/janken.git
$ git push -u origin master

最終的にこのような感じで上げれたら完成です。

19. netlifyの設定をする

次にnetlifyの設定をします。こちらの記事が参考になりますが、この記事でも解説しようと思います。Netlifyは静的なサイトを無料でホスティングしてくれるサービスです。Netlifyのサイトよりアカウントを作成します。githubのアカウントに関連付けて作成します。

この画面に来たら、 New site from Git を選択します。

GitHubを選択します

自分のレポジトリ一覧が出てくるので、先程作ったレポジトリを選択します。
最後にデプロイ設定を以下のようにします。

Build commandはnpm run build
Publish directoryはdistに設定します。

設定が終われば Deploy site を押しましょう!

この画面になれば成功です!

表示されているURLをコピーしておきましょう。

20. wh.imのdevelop画面でゲームの登録をする

wh.imの開発者用画面でアプリを登録します。
アクセスすると、googleでのログイン画面になると思います。(これは、gmailで開発者の認証のためです。)

ここで自分のgoogleアカウントで登録します。
すると次の画面に遷移します。これが開発者アプリ登録画面です。

右上のボタンから、新規アプリを登録します。
その後、必要事項を記入していきます。
ゲームのhostのURLは先程コピーしたURLを記入します。
最後のチェックボックスは色々な人が遊べるように公開したい場合は、チェックをします。(個人的に遊ぶだけの場合はチェックを入れなくて大丈夫です。)

saveを押すと公開完了です!

21. 遊んで見る!

早速遊んでみましょう。まず、IDをコピーします。そして、wh.imに移動します。(今回は開発モードにする必要はありません!)
アプリ選択画面を開き、コピーしたIDを入力しましょう。

これで選べます!
友達と遊んでみましょう!

まとめ

長い記事になりましたが、これでVue.js未経験者の方でもwh.imを使ったゲーム公開までの流れは一通り理解できたかと思います。Vue.jsをもっと学んで行くと、ワードウルフといった複雑なゲームも作れるようになるので、是非挑戦して見てください!

最後に、これらの情報が体系的にまとめた(つもりの)、開発者用ドキュメントも用意しているので、参考にしてみてください。

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

jacaScript mapをfor文で処理する

環境:

GAS

注意点

for (let element of map){}
ではなく、
for (let element in map){}
とすることでelementはmapのkeyとなります。

qiita arrayPractice.js
function Main() {
  const name_map = createMap();
  for (let name in name_map){
    Logger.log(name);
    Logger.log(name_map[name]);
  }
}

function createMap() {
  const this_spread = SpreadsheetApp.getActiveSpreadsheet();
  const criteria_sheet = this_spread.getSheetByName('criteria');
  let name_map = new Map();
  for(let count = 1; count <= 3; count ++){
  name_map[criteria_sheet.getRange(count,1).getValue()] = criteria_sheet.getRange(count,2).getValue();
  }
  return name_map
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【DynamoDB】条件付き書き込み(ConditionExpression)を使ってupdateItemで新規項目を追加しないようにする

DynamoDBのupdate処理で、テーブルに存在しない項目を指定すると、新規項目としてテーブルに追加されてしまいます(UpdateItem)。
条件付き書き込み(ConditionExpression)を使用し、updateで存在しない項目を指定した時に、テーブルに追加されずに何も変化しない状態になるよう実装します。

環境

  • macOS
  • AWS Cloud9
  • Node.js 12

AWS.DynamoDB.DocumentClient

JavascriptでDynamoDBを操作するとき、AWS.DynamoDBを使う方法とAWS.DynamoDB.DocumentClientを使う方法の2パターンがあるのですが、AWS.DynamoDB.DocumentClientを使う方が、DBを操作するときにデータの型を指定せずにコードを書くことができて便利なので、今回はこちらを使用しています。
AWS.DynamoDBとの違いは、データ型の指定が不要な点と、メソッド名がやや異なるくらいで、メソッドの具体的な仕様は全く同じと考えて良さそうです。(AWS.DynamoDB.DocumentClient 公式ドキュメント

実行コード

const AWS = require('aws-sdk');
const DB = new AWS.DynamoDB.DocumentClient();

exports.handler = async(event, context) => {
    const dbParams = {
                        TableName: "tableName",
                        Key:{
                            timestamp: XXXXXX                                
                            message: "message"
                        },
                        ExpressionAttributeNames: {
                            '#s': 'status'
                        },
                        ExpressionAttributeValues: {
                            ':status': true
                        },
                        ReturnValues: 'ALL_NEW', 
                        UpdateExpression: 'SET #s = :status',
                        ConditionExpression: 'attribute_exists(#s)' // ここで条件を指定
                    };
    const data = await DB.update(dbParams).promise();
    return data
}

ここで、

ConditionExpression: 'attribute_exists(#s)' 

によって、「timestamp属性の値がXXXXXX、message属性の値が"message"である項目のうち、statusという属性を持つ項目が存在している場合」のみ、updateが実行されるようになります。
つまり、そもそも指定したtimestampmessageを持つ項目が存在しなければ、何も実行されずに済むということです。

ConditionExpression

ConditionExpressionに使用できる演算子・関数は、
比較演算子: = | <> | < | > | <= | >= | BETWEEN | IN
論理演算子: AND | OR | NOT
関数: attribute_exists | attribute_not_exists | attribute_type | contains | begins_with | size
となっています。(参考:公式ドキュメント

各関数について簡単に説明しておくと、

  • attribute_exists(path):キーで指定した項目にpathという属性が存在する場合に実行
  • attribute_not_exists(path):キーで指定した項目にpathという属性が存在しない場合に実行
  • attribute_type(path, type):pathという属性の値がtypeで指定したデータ型であれば実行
  • begins_with(path, substr):pathという属性の値がsubstrで指定した文字列で始まる場合に実行
  • contains(path, operand):pathという属性の値がoperandを含んでいれば実行
  • size(path):pathで指定した属性のサイズを返す。例えば、size(path) < 3のようにして使う

となっております。
各演算子や関数に関する詳細は、こちらのリファレンスをご覧ください。

実行に失敗したとき

指定したDBの操作がConditionExpressionの制約に反し、実行されなかった場合には、ConditionExpressionという例外が吐かれます。
例外時に何か処理を施したい場合は、try~catchでConditionExpressionをcatchしてあげると良さそうです。

ご参考までに。

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

【DynamoDB】updateItemで新規項目を追加しないようにする 〜条件付き書き込み(ConditionExpression)を使って〜

DynamoDBのupdate処理で、テーブルに存在しない項目を指定すると、新規項目としてテーブルに追加されてしまいます(UpdateItem)。
条件付き書き込み(ConditionExpression)を使用し、updateで存在しない項目を指定した時に、テーブルに追加されずに何も変化しない状態になるよう実装します。

環境

  • macOS
  • AWS Cloud9
  • Node.js 12

AWS.DynamoDB.DocumentClient

JavascriptでDynamoDBを操作するとき、AWS.DynamoDBを使う方法とAWS.DynamoDB.DocumentClientを使う方法の2パターンがあるのですが、AWS.DynamoDB.DocumentClientを使う方が、DBを操作するときにデータの型を指定せずにコードを書くことができて便利なので、今回はこちらを使用しています。
AWS.DynamoDBとの違いは、データ型の指定が不要な点と、メソッド名がやや異なる(〇〇Itemが〇〇になる)くらいで、メソッドの具体的な仕様は全く同じと考えて良さそうです。(AWS.DynamoDB.DocumentClient 公式ドキュメント

実行コード

const AWS = require('aws-sdk');
const DB = new AWS.DynamoDB.DocumentClient();

exports.handler = async(event, context) => {
    const dbParams = {
                        TableName: "tableName",
                        Key:{
                            timestamp: XXXXXX                                
                            message: "message"
                        },
                        ExpressionAttributeNames: {
                            '#s': 'status'
                        },
                        ExpressionAttributeValues: {
                            ':status': true
                        },
                        ReturnValues: 'ALL_NEW', 
                        UpdateExpression: 'SET #s = :status',
                        ConditionExpression: 'attribute_exists(#s)' // ここで条件を指定
                    };
    const data = await DB.update(dbParams).promise();
    return data
}

ここで、

ConditionExpression: 'attribute_exists(#s)' 

によって、「timestamp属性の値がXXXXXX、message属性の値が"message"である項目のうち、statusという属性を持つ項目が存在している場合」のみ、updateが実行されるようになります。
つまり、そもそも指定したtimestampmessageを持つ項目が存在しなければ、何も実行されずに済むということです。

ConditionExpression

ConditionExpressionに使用できる演算子・関数は、
比較演算子: = | <> | < | > | <= | >= | BETWEEN | IN
論理演算子: AND | OR | NOT
関数: attribute_exists | attribute_not_exists | attribute_type | contains | begins_with | size
となっています。(参考:公式ドキュメント

各関数について簡単に説明しておくと、

  • attribute_exists(path):キーで指定した項目にpathという属性が存在する場合に実行
  • attribute_not_exists(path):キーで指定した項目にpathという属性が存在しない場合に実行
  • attribute_type(path, type):pathという属性の値がtypeで指定したデータ型であれば実行
  • begins_with(path, substr):pathという属性の値がsubstrで指定した文字列で始まる場合に実行
  • contains(path, operand):pathという属性の値がoperandを含んでいれば実行
  • size(path):pathで指定した属性のサイズを返す。例えば、size(path) < 3のようにして使う

となっております。
各演算子や関数に関する詳細は、こちらのリファレンスをご覧ください。

実行に失敗したとき

指定したDBの操作がConditionExpressionの制約に反し、実行されなかった場合には、ConditionExpressionという例外が吐かれます。
例外時に何か処理を施したい場合は、try~catchでConditionExpressionをcatchしてあげると良さそうです。

ご参考までに。

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

Web ComponentsでTheme Providerを実装する

Web ComponentsでReactのThemeProviderのようなものを実装したい。

@ionic/coreのthemeの実装を読んだら感銘を受けたのでメモ

まとめ

  • Element.closest(selector)でDOM treeを逆向きに検索する
  • 実装に困ったら@ionic/coreを読む

実装

theme-provider

attribute変更の際にthemeの変更を適用したいという要求が無いなら、空のコンポーネントで良い

<template id="theme_provider">
  <slot></slot>
</template>
customElements.define(
  "theme-provider",
  class extends HTMLElement {
    constructor() {
      super();
      const template = document.getElementById("theme-provider");
      this.template = template.content;

      const shadowRoot = this.attachShadow({
        mode: "open"
      }).appendChild(template.content.cloneNode(true));
    }
  }
);

子コンポーネント

子コンポーネントは何でもいいですが、各テーマのstylesを用意しておきます

<template id="heading_level1">
  <style>
    h1.default {
      color: #333;
    }

    h1.christmas {
      color: green;
    }
  </style>
  <h1>
    <slot></slot>
  </h1>
</template>
customElements.define(
  "heading-level1",
  class extends HTMLElement {
    constructor() {
      super();
      const template = document.getElementById("heading_level1");
      this.template = template.content;

      const shadowRoot = this.attachShadow({
        mode: "open"
      }).appendChild(template.content.cloneNode(true));

      // theme-providerを探してthemeを登録する
      this.themeElement = this.closest('theme-provider');
      const theme = this.themeElement ? themeElement.getAttribute('theme') : '';
      this.shadowRoot.querySelector("h1").className = theme;
    }
  }
);

DOMツリーを親の方向に検索してThemeを取得する

DOMツリーの親方向に探索する場合はElement.closest(selector)

const theme = this.closest('theme-provider').getAttribute('theme');
this.shadowRoot.querySelector("h1").className = theme;

classNameをプログラマブルにしたい場合はclsxとかを使うと良いと思う

theme-providerが変更された場合に反映したい

動的にthemeを入れ替える必要がある場合は、その変更を子コンポーネントに通知したい。

theme-provider側:変更を外に通知するためにCustomEventを使う必要がある。

子コンポーネント側:初回のtheme取得と同じく、親Nodeを遡ってtheme-providerを見つけ、event listenerを登録しておく。

というdemoはCodepenに置きました。

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