20201121のJavaScriptに関する記事は24件です。

【Vue.js】カウンター作成記録

はじめに

vueの練習したいけど何を作ればいいのか。
そうだ、カウンターを作ろう。

とりあえず作った


カウンター (2).png

コード

counter.html
<template>
  <div>
    <b-button v-on:click="countUp" variant="success">カウントアップ</b-button>
    <b-button v-on:click="countDown" variant="primary">カウントダウン</b-button>
    <b-button v-on:click="countClear" variant="danger">カウントクリア</b-button>
    <h1>{{displayCount}}</h1>
    <div v-if="displayCount > 10">
      {{successCount()}}
    </div>
  </div>
</template>
counter.js
<script>
export default {
  data() {
    return {
      displayCount: 0
    };
  },
  methods: {
    countUp() {
      this.displayCount++;
    },
    countDown() {
      this.displayCount--;
    },
    countClear() {
      this.displayCount = 0;
    },
    successCount() {
      alert('great!!')
    }
  }
};
</script>

さいごに

マウスをカチカチするだけでも楽しくなっちゃう自分としては
かなり良い暇つぶしグッズを作ってしまった。
vue.jsで作った意味があるのかわからない感じだけど、記法とかをおぼえる練習としては良かったかな。
ここまで読んでくれた方ありがとうございました!

おまけ

下記のリンクで遊べます。
ちなみにCodeSandboxはかなり優秀な開発環境ですので是非使ってみてほしいです。
https://codesandbox.io/s/counter-practice-xlquf

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

Nuxt.jsビギナーズガイドで「Request failed with status code 401」にハマった

スクリーンショット 2020-11-21 15.40.56.png

Nuxt.jsビギナーズガイド第二章にて上記エラーに2時間ほどハマったので。

正誤表がありました。

正誤表

本書では、第二章で Qiita API へとアクセスを行いますが、その際のトークン指定方法に誤りがあったため、以下のように設定するようにお願いたします。

正: config.headers.common['Authorization'] =`Bearer ${process.env.QIITA_TOKEN}`
誤: config.headers.common['Authorization'] = process.env.QIITA_TOKEN


他にも何かと誤植があるので要チェックですね。

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

JavaScriptで2段階に連動するプルダウンの作成と要素の表示、非表示

備忘録として残しておきます

HTML

choice.html
<select id="parent" onchange="date()" class="form">
  <option value="和食">和食</option>
  <option value="洋食">洋食</option>
  <option value="中華">中華</option>
  <option value="" disabled selected>選択してください</option>
</select>

<select id="children" class="form">
  <option value="" disabled selected>選択してください</option>
  <option value="味噌汁">味噌汁</option>
  <option value="肉じゃが">肉じゃが</option>
  <option value="天ぷら">天ぷら</option>

  <option value="グラタン">グラタン</option>
  <option value="オムライス">オムライス</option>
  <option value="ナポリタン">ナポリタン</option>

  <option value="麻婆豆腐">麻婆豆腐</option>
  <option value="餃子">餃子</option>
  <option value="春巻">春巻</option>
</select>

CSS

choice2.css
#children option:disabled{
  display:none;
}
#children option{
  display:block;
}

JavaScript

choice3.js
function date() {

//id="course"の値を取得
var parentlement = document.getElementById( "parent" ) ;

//id="seminardate"を取得
var childrenDate = document.getElementById( "children") ;

   //disabledをtrueに
  for (var i = 0; i < childrenDate.length; i++){
       childrenDate.options[i].disabled = true;
  }
    // disabledに代入
     if( parentlement.value == "和食") {
       childrenDate.options[1].disabled = false;//味噌汁
       childrenDate.options[2].disabled = false;//肉じゃが
       childrenDate.options[3].disabled = false;//天ぷら

    } else if( parentlement.value == "洋食") {
       childrenDate.options[4].disabled = false;//グラタン
       childrenDate.options[5].disabled = false;//オムライス
       childrenDate.options[6].disabled = false;//ナポリタン

    } else if ( parentlement.value == "中華") {
       childrenDate.options[7].disabled = false;//麻婆豆腐
       childrenDate.options[8].disabled = false;//餃子
       childrenDate.options[9].disabled = false;//春巻

     } else if ( parentlement.value = "") {
       childrenDate.disabled = true;
    }
}

和食を選択します。
スクリーンショット (35).png
和食を選択すると「味噌汁」「肉じゃが」「天ぷら」以外にdisabledが付きます。

disabledが付いたoptionは非表示になります。

choice2.css
#children option:disabled{
  display:none;
}

display:none;がなければこうなります。↓
スクリーンショット (38).png

まとめ

このやり方でも一応動きますが、これではメンテナンスがしにくいのでJavaScriptの改善が必要みたいです。

参考リンク

https://developer.mozilla.org/ja/docs/Web/CSS/:disabled

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

【初心者でもわかる】セレクトボックスの選択で、リンク先を変更する方法

どうも7noteです。セレクトボックスで選択された内容ごとにリンク先を変える方法

シンプルなスクリプトで対応が可能です。
検索ボタンを押したときに、リンクする方法と、セレクトの値を変更した瞬間にリンクする方法の2種類のサンプルソースです。

※jQueryを使用しています。jQueryってなんだという方は[こちら](https://qiita.com/7note/items/45bd8efbedd557667218)

「検索」ボタンありバージョン

index.html
<select name="preflist">
  <option value="/hokkaido/">北海道</option>
  <option value="/okinawa/">沖縄</option>
</select>
<button class="btn">検索</button>
script.js
$(".btn").on("click",function(){
  const selected = $("select[name=preflist]");
  window.location.href = selected.val();
});

検索ボタンなしバージョン(選択を切り換えたらすぐリンク)

index.html
<select name="preflist">
  <option value="/hokkaido/">北海道</option>
  <option value="/okinawa/">沖縄</option>
</select>
script.js
const selected = $("select[name=preflist]");
selected.on("change",function(){
  window.location.href = selected.val();
});

解説

window.location.hrefでリンク先を指定することができます。
タイミングはボタンありの場合はボタンを押した時、ボタンなしならセレクトの値が変更された時に発火するように作ります。
とび先のリンクを各optionタグのvalueに入れておくことで、指定のリンク先へ飛ばすことができます。

参考
https://awesome-linus.com/2019/05/09/select-option-href/

おそまつ!

~ Qiitaで毎日投稿中!! ~
【初心者向け】HTML・CSSのちょいテク詰め合わせ

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

簡単 (電子) レシート receiptline の新機能を試してみた

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

少し前にマイナーバージョンアップがあったので、新機能を試してみました。
(2020年11月21日時点で Verion 1.1.1)

Web フォントで表示

前回の記事で試していた Google Fonts の「Kosugi Maru」が採用されました。
これで iOS, Android, Windows, Linux, Mac どの環境でも同じ表示になるはずです。
00.png

データは過去の記事から。

ReceiptLine
{image:iVBORw0KGgoAAAANSUhEUgAAASAAAAAwAQMAAACL7DXsAAAABlBMVEUAAAD///+l2Z/dAAABqElEQVQ4y+3VMW7bMBQGYLIqwqUo10ymj5AxBQwouUmO0NEFBFOCBo05Qs7QG5DV0K1noOGhKwMvLEDw7yOVALEUIB46BdUofJTe+x8pMZxxsXeFol4gJucm1HMUopght0SPmCO2RCOGGTJLNBB783UShzladidxfBMluYjpFaT+FYqEKE4O/8VtRA7WrRO8Sl+pEIPWq2Nd0LGPHvc81CJYYGgTfslwdy2wql1UP1RBv+2foFVHaO8AaYAHYW4uq1hLF1dsQj/tY9wpS2j0hAgq4TZVXkVP+jChwfrU1BPSSTkkJfxO2KAHQv1U04P1OhbUMh0z0oQk3e0zmrpD50UoqKdWCSGdopxT4oGfojBDNBZvIztF4ynKA95bsG1B3RMaFugw2qPfqmeUI5AzNODQU06NzMgkqpm6m9BTBGX7jn33PTYUy0qYBqki9PmiIApzW9Aeol1fxcR45MJxmjlDYm1GNBb/saDQCHO3/QZ2GyvuK6BdK/AuI6yU/yT6cjjVPqQK1kKqSIE42vPDwVxWTBvEzY69POb2nG/Bf4T2HMT0u/sj/AWoqMYMK2VNkgAAAABJRU5ErkJggg==}|
^^領収書|
2020/07/20 01:23|
#NGC17TH|

缶ビール | ¥211~
シュークリーム | ¥129*
-
小計 | ¥340~
(10%対象 | ¥211)
( 8%対象 | ¥129)
(内消費税等 | ¥28)
^合計 | ^¥340
お 預 り | ¥1,000
お 釣 り | ¥660

*印は軽減税率対象商品です。

USB (Bluetooth) プリンターで印刷

仮想シリアルポート対応 USB (Bluetooth) プリンターでテスト印刷できるようになりました。

そこで、ネットオークションで新たに SII RP-E10 と CITIZEN CT-S255 を入手。
どちらも USB 接続モデルで、店舗で予備機として保管されていたものだそうです。
01.jpg

仮想シリアルポートの設定

RP-E10 用「SII RP Series Communication Software for Windows」をインストール。
02.png

「RP通信設定ユーティリティ」で仮想シリアルポートドライバーに切り替え。
03.png

CT-S255 用「シチズンサーマルプリンタ仮想 COM ポートドライバー」をインストール。
04.png

デバイスマネージャーで確認。RP-E10 は「COM3」、CT-S255 は「COM7」になりました。
05.png

Serial-LAN Converter の設定

servers.json で、 COM3 (または COM7) と TCP 9100 ポートをつなぎます。

servers.json
"serial": {
    "host": "127.0.0.1",
    "port": 9100,
    "device": "COM3"
}

ReceiptLine Designer の設定

printers.json に、RP-E10 と CT-S255 を登録。localhost がプリンターになります。

printers.json
{
    "rp_e10": {
        "host": "127.0.0.1",
        "port": 9100,
        "cpl": 48,
        "encoding": "cp932",
        "gamma": 1.8,
        "upsideDown": false,
        "spacing": false,
        "cutting": true,
        "command": "sii"
    },
    "ct_s255": {
        "host": "127.0.0.1",
        "port": 9100,
        "cpl": 48,
        "encoding": "cp932",
        "gamma": 1.8,
        "upsideDown": true,
        "spacing": false,
        "cutting": true,
        "command": "escpos"
    }
}

テスト印刷

Node.js でサーバーを起動。ReceiptLine Designer で印刷できました。
この開発ツールの使い方はこちら

$ node designer.js

06.jpg

新しい印刷オプション

printers.json で行間隔と用紙カットの有無が選択できるようになりました。

行間隔

spacingtrue にすると、行間が空いて読みやすくなります。
ただし、罫線もしくは縦倍角以上の拡大文字がある行の間隔は変わりません。
07.jpg
ちなみに、前々回の記事は、上下反転印刷に対応できない中途半端な改造でした…

用紙カット

cuttingfalse にすると、印刷完了後に自動カットされなくなります。
08.jpg

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

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

効果的なTypeScriptを読んで学んだ4つのこと

こちらの記事は、Kaylie Kwon 氏により2020年1月に公開された『 4 Things I Learned from Reading Effective TypeScript 』の和訳です。
本記事は原著者から許可を得た上で記事を公開しています。

効果的なTypeScriptを読んで学んだ4つのこと

壁の前で本で彼女の顔を覆っている

Photo by Siora Photographyon Unsplash

数日前、私は長時間のフライトでダン・ヴァンダーカム氏の効果的なTypeScript(以下TSとする)を読み終えました。(正確には8時間!)
その本は非常に実践的なアドバイスで簡潔に書かれています。本を読んでいただくことをお勧めしますが、ここに私のお気に入りのコツがいくつかあるのでご紹介します。

1.エラーによるオブジェクトの変更を防ぐため、“ readonly “を使用すること

配列またはタプルがreadonlyとしてマークされている場合、ユーザーがそれらのオブジェクト内の項目を追加、削除、または更新しようとするとTSはエラーが発生。これは副作用を回避するための関数型プログラミングで特に役立つ。

画像1

2.型の再利用性については関数宣言よりも関数式を優先すること

同じ関数シグネチャを持つユーティリティを書き込むと少し繰り返しになる可能性がある。
以下に、入力タイプと出力タイプを毎回再度書き出す必要がある3つのカート関連関数を示す。

画像2
DRY原則を維持するにはシグネチャ全体を関数型に割り当て、関数式の使用に切り替えることができる。

画像3

3.より良い型安全性を得るためにどれよりもunknownを好むこと

anyを使用すれば便利なエスケープハッチになる可能性があるが、それを回避する正当な理由もある。どんな型安全性も保証するものではなく、コールチェーンの上位のコンシューマーからエラーを隠す。
型がunknownの場合にはTSは存在しないメソッドを呼び出す等のエラーを正しく表示する。

画像4

開発者がコンパイラよりも型についての知識が豊富な場合は意図を明確にするためにダブルアサーションと共にunknownを使用することもできる。

画像5

4.より良い開発経験のためにJSDocもしくはTSDocスタイルのコメントに傾倒すること

ほとんどの場合においてコメントに型情報を含めることは避けるべき。ただし、移行し、JSを使用してTSを有効にする場合、または関数に関する追加情報を提供したい場合にはJSDocもしくはTSDocが型チェックを行い、IDEと統合するための優れた方法だ。

画像6

注意:VSCodeは適切にフォーマットされたコメントをツールチップに表示する。

画像7


これでおしまい!
これらのコツを選択したのは既存のコードベースの改善にすぐに適用できるから。お役に立てば幸いです。

翻訳協力

Original Author: Kaylie Kwon
Original Article: 4 Things I Learned from Reading Effective TypeScript
Thank you for letting us share your knowledge!

ご意見・ご感想をお待ちしております

今回の記事は、いかがだったでしょうか?
・こうしたら良かった、もっとこうして欲しい、こうした方が良いのではないか
・こういったところが良かった
などなど、率直なご意見を募集しております。
いただいたお声は、今後の記事の質向上に役立たせていただきますので、お気軽にコメント欄にてご投稿ください。Twitterでもご意見を受け付けております。
みなさまのメッセージをお待ちしております。

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

【DroidScript】スマホでスマホアプリを作る シリーズまとめ

概要

DroidScriptというアプリを使うと、スマホからjavascriptベースの記述でスマホアプリを作れてしまうらしい。

DroidScriptでググっても日本語のサイトはあまりヒットしないし、Qiitaの記事もゼロ・・・面白そうなアプリなのに(´・ω・`)

以前にWEBアプリ導入の練習で「電子レンジワット数で加熱時間を換算するアプリ」を作り、今回も同じお題で練習してみたので、まとめてみた。

参照:【GAS】電子レンジワット数で加熱時間を換算するwebアプリを作ってみた

これまでのまとめ

スマホでスマホアプリを作る① Hello World!
DroidScriptの紹介と、サンプルアプリの実行。

スマホでスマホアプリを作る② 背景やテキストの追加
オリジナルアプリ作成開始。背景画像やテキストの追加。

スマホでスマホアプリを作る③ スピンボタンとシークバー
計算用の数字を取得するためのボタン設置。

スマホでスマホアプリを作る④ 実行ボタン
取得した数字を元に計算実行。アプリ完成!

自作アプリの動作

アプリの画面がこちら。
Screenshot_20201121-154732.png
実際に動かしてみたところ。
20201121_181900.gif
自分のレンジのワット数を設定。温める対象のラベルに記載されたワット数と加熱時間を設定。計算ボタンをタップすると、自分のレンジで温める場合の加熱時間を教えてくれる。

GIFには出てこないけど、ドロイド君をタップするとアプリの情報がポップアップする。

コード全文

function OnStart() 
{ 
  lay = app.CreateLayout( "linear", "VCenter,FillXY" ); 
  lay.SetBackground( "/Sys/Img/BlueBack.jpg" ); 

  txt = app.CreateText( "HOW MANY MIN" ); 
  txt.SetTextSize( 22 ); 
  lay.AddChild( txt ); 

  img = app.CreateImage( "/Sys/Img/Icon.png", 0.1 ); 
  img.SetMargins( 0, 0.02, 0, 0 ); 
  img.SetOnTouchDown( img_OnTouchDown ); 
  lay.AddChild( img ); 

  txt = app.CreateText( "Your Microwave" ); 
  txt.SetMargins( 0,0.02,0,0 ); 
  txt.SetTextSize( 20 ); 
  lay.AddChild( txt ); 

  my = app.CreateSpinner( "730,700,680", 0.3 ); 
  my.SetOnTouch( spin_my ); 
  lay.AddChild( my ); 

  txt = app.CreateText( "Label Microwave" ); 
  txt.SetMargins( 0,0.02,0,0 ); 
  txt.SetTextSize( 20 ); 
  lay.AddChild( txt ); 

  label = app.CreateSpinner( "500,1500", 0.3 ); 
  label.SetOnTouch( spin_label ); 
  lay.AddChild( label ); 

  txt = app.CreateText( "Label heating time" ); 
  txt.SetMargins( 0,0.02,0,0 ); 
  txt.SetTextSize( 20 ); 
  lay.AddChild( txt ); 

  skb = app.CreateSeekBar( 0.8 ); 
  skb.SetOnTouch( skb_OnTouch ); 
  skb.SetRange( 600 ); 
  skb.SetValue( 180 ); 
  lay.AddChild( skb ); 

  btn = app.CreateButton( "EXECUTE", 0.4 ); 
  btn.SetMargins( 0, 0.05, 0, 0 ); 
  btn.SetOnTouch( btn_OnTouch ); 
  lay.AddChild( btn ); 

  app.AddLayout( lay ); 
} 

function img_OnTouchDown( ev ) 
{ 
    app.ShowPopup( "I will calculate the heating time!" ); 
    app.Vibrate( "0,100,30,100,50,300" ); 
} 

function spin_my( item ) 
{ 
  app.ShowPopup( "Set " + item + "W" ); 
  app.Vibrate( "0,30" ); 
} 

function spin_label( item ) 
{ 
  app.ShowPopup( "Set " + item + "W" ); 
  app.Vibrate( "0,30" ); 
} 

function skb_OnTouch( value ) 
{ 
  value = Math.round( value / 5 ) * 5; 
  var min = Math.floor( value / 60 ); 
  var sec = Math.floor( value % 60 ); 
  sec = ( "0" + sec ).slice( -2 ); 

  var settime = min + ":" + sec; 
  app.ShowPopup( "Set Time = " + settime ); 
  app.Vibrate( "0,10" ); 
} 

function btn_OnTouch() 
{ 
  var my_wave = my.GetText(); 
  var label_wave = label.GetText(); 
  var time = skb.GetValue(); 

  time = Math.round( time * 0.2 ) / 0.2; 

  var ratio = ( label_wave / my_wave ); 
  var after = time * ratio; 

  var remain = after % 60; 
  var newmin = ( after - remain ) / 60; 
  var newsec = Math.round( remain ); 
  newsec = ( "0" + newsec ).slice( -2 ); 

  var ans = newmin + ":" + newsec; 

  app.ShowPopup( "Heat for " + ans ); 
  app.Vibrate( "0,100,30,100,50,300" ); 
}

DroidScriptの操作Tips

PCでコード編集

左上のWiFiマークをタップすると、画面のような表示が表れる。
Screenshot_20201121-183559.png
http://androidscript.org/ide
からアクセスして、伏字のIPアドレスとパスワードを入力すればPCからコードを編集できる。
DroidScript.jpg

スマホでコード編集

感覚的には、スマホでコピペしようと思ったらカーソル長押しなんだけど、こちらのアプリだと操作が異なるのが最初分からなくて苦労した・・・。

カーソルを操作する丸いアイコンをダブルタップすると、コピペなどのメニューが開く。これでサンプルコードから必要な部分をコピーすることができる。
Screenshot_20201121-183622.png

以上!

おしまい

シリーズ

DroidScript電子レンジアプリ作成
スマホでスマホアプリを作る① Hello World!
スマホでスマホアプリを作る② 背景やテキストの追加
スマホでスマホアプリを作る③ スピンボタンとシークバー
スマホでスマホアプリを作る④ 実行ボタン
スマホでスマホアプリを作る シリーズまとめ

外伝
【GAS】電子レンジワット数で加熱時間を換算するwebアプリを作ってみた

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

【Jest】テスト用のAPIとDBとうまく付き合っていく方法

始めに

apiのnodeのバージョンを8から14にしました(まだリリースはしてない)!!
14への移行自体は簡単だったもののテストの移行でかなり苦戦しました。。。
テストは古のライブラリmocha-coで書かれており、仕方なくmocha-coのシンタックスをjestに置き換える作業を開始しました。これが予想以上にめんどくさくその時のHow toを解決する方法がこの記事になります。
以下の問題にお悩みの方はこちらを参考していただけるかと思います。またいい方法をご存知の方はお教えください!!

  • TooManyConnectionとなってしまう
  • 毎回のテストでサーバー立ち上げとクローズをするのめんどくさい
  • --ranInBandつけたけどなんか挙動がよくわからない・・・。
  • mochaからのjestに移行したい

正確にjestのランタイムの仕様が分かっているわけではなく、調べたり試したりした結果なので間違いやもっと詳細が分かる方がいらっしゃいましたらぜひお教えください!!
また正確なコードを書くと記述量が多いので雰囲気で書いているところもあるのでご了承ください。

前提条件

  • テスト用のDBを立ててデータを流しこみながら行っている
  • モジュール単位のテストではなくApiに対してリクエストを投げてそのレスポンスとDBのデータを確認してテストを行う

といったことを前提とします。データベースはインメモリではなくデータベースサーバーを立てる前提です。ですのでテストは順次実行をしていく必要があります。(もともとそうなっていたのが理由としては大きいですが)実際の環境に近い環境を立てることができることがこの作りのメリットだったりします。逆にインメモリにすると並列でテストを実行することができるので、実行速度をあげることができます。

では、データベースサーバーを立てるテストの場合、順次実行をしていかないといけない理由としては、

スクリーンショット 2020-11-21 13.17.32.png

こんな感じに一斉に更新がかかると他のテストに影響が出ることがあります。
たとえば、テスト前にテーブルを消したり、データの更新をしたりとか・・・。すると想定されていた状態にないデータが生まれテストが落ちたします。しかも恐ろしいことにそれが処理時間によって変わるのでテストガチャが生まれます。
そのためこの場合は、順次実行をしていくのがよいです。

スクリーンショット 2020-11-21 13.21.50.png

こうすればテストファイルが一斉に更新されることによっておこるデータベースの不整合を防ぐことができます。
そのためのオプションとして、jestでは--runInBandオプションを利用します。

jest --runInBand

ただし、これだけでは問題が発生します。

テスト用のAPIサーバーが立ち上がらない or 立てたり切ったりし続けると遅い問題

具体的にどういう問題かについての前にAPIテストで行うことを書きます。

APIテスト

APIのテストでは

  • モジュール単位でテストする方法
  • APIサーバーを立ててリクエストを送ることによって

があります。後者の方法で有名なライブラリとしてはsupertestが挙げられます。
例えばexpresssupertestの両方を使ってサーバーを立てるコードは下記のように書けます。

// app.js
const express = require('express');
const app = express();

app.get('/', function (req, res) {
    res.send('Hello World');
}); 
module.exports = app;
// test-request.js
const supertest = require('supertest');
const app = require('./app');

module.exports = supertest.agent(app.listen(3000));
// index.test.js
const testRequest = ('./test-request');

describe('test', () => {
    it('200?', () => {
        testRequest
            .get('/')
            .expect(200);
    });
});

またテスト用のデータを流し込むためのモジュールも下記のように定義します。

// test-model.js
const Sequelize = require('sequelize');
const sequelize = new Sequelize('test', 'test', 'test', {
    host: '127.0.0.1',
    dialect: 'mysql',
});

const models = {...};

module.exports = {
    sequelize: sequelize,
    models: models
};

mochaの場合はtest-requestやtest-modelを読み込んでテストをすればそれでよく、問題は発生しません。
しかし、jestの場合はそうはいきません。

mochaにおける挙動

スクリーンショット 2020-11-21 18.53.43.png

といった形になります。そのため先ほどのコードで一度しかコネクションはできないですし、一度しかサーバーは立ち上がりません。

jestにおける挙動と問題

jestにおいてはどのようになるのかというと、

スクリーンショット 2020-11-21 18.54.12.png

といった形になります。そのためmochaのときのような書き方はやめてテストファイルごとに『立てて、切って』とする必要があります(書き換えの際はめっちゃ大変です)。
とはいえ、毎回そのようにするのは結構大変ですし、毎回『起動、コネクト、停止、切断』と実行していくとテスト完了にかかる時間が増えてしまいます。

  • 毎回のテストでサーバーの起動・停止、dbへのコネクション・停止をする必要がある
    • 記述量が増え、closeを忘れるとテストが落ちる
    • 処理が増えるため必然的に実行時間が長くなる
  • エラーハンドリングをしてcloseしなくても動くようにする
    • 大量のコネクションが貼られることになりtoo many connectionとなってしまう

といった問題が発生します。

そのためには、globalSetupとtestEnvironmentを使います。

globalSetupを使い一度のみ起動する

globalSetupを使います。

スクリーンショット 2020-11-21 18.54.37.png

jestではこういった形で各ファイルが実行されるため、一度しか実行されないglobalSetupでサーバーの起動をなどを行うのが良いかと思われます。

// globalSetup
const supertest = require('supertest');
const app = require('./app');

global.__request = supertest.agent(app.listen(3000));
// test-utils/request.js
module.exports = global.__request;
// test.ts
import request from 'test-utils/request';

describe('test', () => {
   it('200', () => {
     request.get('/').expect(200);
   });
});

気持ち的にこんな感じにglobalにロードしたモジュールを保存させてテストで利用できるようにしたいんですが、ここにはここで落とし穴があります。その落とし穴はsetUpFilesにあります。
setUpFilesでは何を行っているのかというか『あたらしい環境のセットアップです』具体的にはglobalをはじめとしたテスト実行用の環境を生成するタイミングになり、新しくglobalを作っているため、下記のようなコードの場合、

// globalSetUp
global.__global = 'I am from global setup';
// setUpFiles
console.log(global.__global); // <- 新しいコンフィグになるためundefined
global.__setUp = 'I am from setup files'; // <- ただしここで追加したglobalメンバーにはテストファイルがアクセス可能

となります。そのため、実際のテストファイルでも

// test.js
describe('test', () => {
  console.log(global.__global); // undefined
  console.log(global.__setUp); // I am from setup files;

となります。
親玉のランナーの実行環境とは別の環境が作られるというところがポイントになります。
setUpAfterEnvもタイミングは異なれど同じ挙動をします。

そこでどうするのかというとtestEnvironmentを利用します。

testEnvironment

testEnvironmentを使うことで柔軟にテスト環境を構築することができます。

// package.json
    "testEnvironment": "./my-custom-environment.js",
// my-custom-environment
const NodeEnvironment = require('jest-environment-node');

class CustomEnvironment extends NodeEnvironment {
  constructor(config, context) {
    super(config, context);
  }

  async setup() {
    this.global.__request = global.__request;
    await super.setup();
  }
}
module.exports = CustomEnvironment;

このようにsetupメソッドでglobal.__requestthis.global__request に設定することによってテストファイルの方で global.__requestにアクセスすることができます。

まとめ

最終的にはこのようになります。

アプリケーションコード

// app.js
const express = require('express');
const app = express();

app.get('/', function (req, res) {
    res.send('Hello World');
}); 
module.exports = app;
// model.js
const Sequelize = require('sequelize');
const sequelize = new Sequelize('test', 'test', 'test', {
    host: '127.0.0.1',
    dialect: 'mysql',
});

const models = {...};

module.exports = {
    sequelize: sequelize,
    models: models
};

テストユーティリティコード

// test-request.js
module.exports = global.__request;
// test-model
module.exports = global.__db;
// global-setup.js
const supertest = require('supertest');
const app = require('./app');
const db = require('model');

global.__request = supertest.agent(app.listen(3000));
global.__db = db;
// my-custom-environment
const NodeEnvironment = require('jest-environment-node');

class CustomEnvironment extends NodeEnvironment {
  constructor(config, context) {
    super(config, context);
  }

  async setup() {
    this.global.__request = global.__request;
    this.global.__db = global.__db;
    await super.setup();
  }
}
module.exports = CustomEnvironment;
// jest.config.json
{
    "testEnvironment": "./global-setup.js",
    "globalSetup": "./my-custom-environment.js"
}

テストコード

// test.js
const testRequest = ('./test-request');
const testModel = ('./test-model');

describe('test', () => {
    beforeAll(async () => {
      await testModel.reset();
    });
    it('200?', () => {
        testRequest
            .get('/')
            .expect(200);
    });
});

全体図

問題点

ただし、jestのmockと相性が悪いという問題点があります・・・。
というのもglobal上にサーバーコードがロードされているため、それらを実行したい時は各ファイルごとに一回サーバーを落としてもう一度ロードする必要があります。
そのため、globalにこれらをやるための入り口を開けてあげる必要がありました。
ここら辺みなさんどうしてるんですかね?いろいろとご意見伺いたいのもあって書かせていただきました。

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

webStorageとcookie

はじめに

今までも何度かlocalStorageやsessionStorageは、見てきました。そしてそれらに対してブラウザ側でいくつかデータを保持できるものという認識でしかなかったのですが、なんとなくの違いなどがわかりましたので、記事にしました。

WebStorage

5メガバイトまで保持できる

sessionStorageとlocalStorageの総称

2つの違い

基本的に機能や用意されているメソッドは変わりません。
しかしこれら二つの中で具体的な違いは、その有効範囲になります。
sesstionStorageは、タブやウィンドウを変えてしまうと、データは保持できなくなります
localStorageは、タブやウィンドウを変えても、データを保持できます(半永久的)同じオリジン内であれば

オリジン: プロトコルとドメイン名とポート番号の組み合わせ(URL)

主なメソッド

setItem()・・・これは、実際にブラウザでデータを保持させるときのメソッド

getItem()・・・データを呼び出すときに使う

clear()・・・全削除

どちらも機能やメソッドは変わらない、変わるのは範囲だけなのでよりlocalStorageが使うのが妥当なのかな?

cookie

機能はほぼwebStorageと変わらない

webStorageとの違い

・http通信で情報を付与してサーバへリクエストできる。
・cookieは有効期限がある。
・cookieは拒否もできる機能もある
・4KBまでしか保持できない

通常はwebStorageを使い、サーバーへのリクエストで情報を付与させたい時などはcookieを使う

注意点

基本的にこれらの保持されたデータは、デベロッパーツールで見ることができるので、大切な情報や見られたくない情報は、保持させておくべきでない

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

【DroidScript】スマホでスマホアプリを作る④ 実行ボタン

あらすじ

DroidScriptというアプリを使うと、javascriptベースの記述でスマホアプリが作れるらしいので、練習として「電子レンジワット数で加熱時間を換算するアプリ」を作る。

前回までのおさらい

スピンボタンとシークバーを実装し、計算に必要なデータ入力が揃った。あとは計算するだけ。
Screenshot_20201121-154732.png

スマホでスマホアプリを作る① Hello World!
スマホでスマホアプリを作る② 背景やテキストの追加
スマホでスマホアプリを作る③ スピンボタンとシークバー

ボタンの設置

btn = app.CreateButton( "EXECUTE", 0.4 ); 
btn.SetMargins( 0, 0.05, 0, 0 ); 
btn.SetOnTouch( btn_OnTouch ); 
lay.AddChild( btn );

app.CreateButtonでボタンを作成。括弧内でボタンに表示する文字と、画面幅に対するサイズを設定。.SetMarginsで直前に設置したシークバーとの間隔を設定。.SetOnTouchでタップしたときに呼び出す関数btn_OnTouchを設定している。

そのコードがこちら。

function btn_OnTouch() 
{ 
  var my_wave = my.GetText(); 
  var label_wave = label.GetText(); 
  var time = skb.GetValue(); 

  time = Math.round( time * 0.2 ) / 0.2; 

  var ratio = ( label_wave / my_wave ); 
  var after = time * ratio; 

  var remain = after % 60; 
  var newmin = ( after - remain ) / 60; 
  var newsec = Math.round( remain ); 
  newsec = ( "0" + newsec ).slice( -2 ); 

  var ans = newmin + ":" + newsec; 

  app.ShowPopup( "Heat for " + ans ); 
  app.Vibrate( "0,100,30,100,50,300" ); 
}

スピンボタンやシークバーで操作した数字をどうやって拾ってくるのか分からなかったけど、操作後の状態から単純に参照できた。

スピンボタンで選んだ選択肢は.GetText()で、シークバーで合わせた数字は.GetValue()で参照。

シークバーを操作した時に表示されるのが5秒間隔なので、計算するためにシークバーから参照した数字も5刻みに丸める。

あとはワット数の比で加熱時間を換算し、m:ss表示に加工して"Heat for " + ansでポップアップ、仕上げにapp.Vivrateでスマホを振動させ達成感を味わう。
20201121_181900.gif

補足

スピンボタンの選択肢を.GetTextで参照しているので、受け取った数字が文字列になっていると考えて、数字型に変更するためにNumber()メソッドを使ったところエラーになってしまった。

結局、そんなことしなくてもそのまま計算できてしまうみたい^^)b

次回

アプリ作成はこれにて終了。

次回はシリーズ目次的なまとめと、コード全文、DroidScriptのちょっとした使い方を紹介。
スマホでスマホアプリを作る シリーズまとめ

つづく

シリーズ

DroidScript電子レンジアプリ作成
スマホでスマホアプリを作る① Hello World!
スマホでスマホアプリを作る② 背景やテキストの追加
スマホでスマホアプリを作る③ スピンボタンとシークバー
スマホでスマホアプリを作る④ 実行ボタン
スマホでスマホアプリを作る シリーズまとめ

外伝
【GAS】電子レンジワット数で加熱時間を換算するwebアプリを作ってみた

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

Express×Helmetでウェブセキュリティを学ぶ

この記事は エニプラ Advent Calendar 2020 の 11 日目の記事です。

はじめに

Express 公式の『実稼働環境における Express のセキュリティーに関するベスト・プラクティス』というドキュメントでは、「脆弱性対策のために Helmet を使ってね(要約)」と書いているけれど、どんなリクエストヘッダが設定されて、それによってどんな効果があるのか具体的に把握していなかったので、勉強がてらに調べてみました。

私自身セキュリティには勉強中の身であり、ことセキュリティに関しては内容の正確性が求められると思いますので、誤りや勘違いなどがございましたらどうぞご指摘頂ければ幸いです:bow_tone1:

Helmet とは??

HelmetExpress で作成された Web アプリケーションに対して HTTP ヘッダを設定することで、webアプリケーションをセキュリティ的に堅牢化するライブラリです。
(Express に限らず Koa 用のラッパーもあるようです)

ただ、公式で

It's not a silver bullet, but it can help!

とあるように、必ずしもセキュリティ的な問題を一挙に解決してくれる万能薬ではありません。

実際、おそらく CSRF やセッションハイジャック、SQL インジェクションなどへの対策としては有効でないと思われますが、XSS やクリックジャッキングなどの対策にはなると思われます。

Helmet を実際に試してみる

それでは、さっそく Helmet を使ってみましょう。

環境は以下の通りです。

Node.js npm Express Helmet
v14.15.0 6.14.8 4.17.1 4.2.0

Express と Helmet をインストール

新しいフォルダを作り、Express と Helmet をインストールします。

> mkdir express-helmet-sample
> cd express-helmet-sample
> npm init
> npm install --save express helmet

Helmet を使う前のレスポンスヘッダを見てみる

新しく作ったフォルダ内に、app.ts ファイルを作成し、公式を参考にコードを記述します。

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log('listening on port:', port)
})

サーバーを起動して、ポート3000で接続を待ち受けるコードです。
ルートURL(/)に対して HTTP GET リクエストを投げてみます。
app.js を実行してサーバを起動した後、

> node app.js

--dump-header 付きの curl コマンドを実行し、起動したサーバのルート URL から HTTP GET レスポンスヘッダを取得します。

> curl --dump-header - http://localhost:3000

取得したレスポンスヘッダは以下の通りです。

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 12
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Date: Fri, 13 Nov 2020 15:05:20 GMT
Connection: keep-alive
Keep-Alive: timeout=5

特に何の変哲もなさそうですが、X-Powered-By ヘッダを見ると、このレスポンスが Express によって作られたアプリから返されていることがが分かります。この場合、攻撃者はサーバが Express によって起動されていると分かるので、攻撃前のひと手間が省かれてしまうという懸念になります。
ただ、単に Express だと分かったところで何ができるかは微妙なところではないかな?と思ってしまいますが……。

しかし、MDN でも以下のように注意書きがなされているため、X-Powered-By ヘッダは無くしたほうが無難だと思われます。

ホスティング環境やその他のフレームワークによって設定される可能性があり、アプリケーションや訪問者に有益ではない情報を含みます。潜在的な脆弱性が発現することを防ぐために、このヘッダーは設定しないでください。

ここでは、単に Express サーバを起動しただけでは、X-Powered-By のようなリスクのあるヘッダがそのままレスポンスに含まれてしまうことに注意してみます。

Helmet を被ってみる

ここでヘルメットを使用してみます。
以下のようにコードを変更します。

const express = require('express')
const helmet = require('helmet')
const app = express()
const port = 3000

app.use(helmet()) // ヘルメットを使用する

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log('listening on port', port)
})

サーバを再起動し、改めて curl コマンドによりレスポンスヘッダを取得します。

> node app.js
> curl --dump-header - http://localhost:3000

レスポンスヘッダの中身を見てみると、先ほどとは様変わりしていることが伺えます。
先ほど言及した X-Powered-By が消えていますね。

HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
X-DNS-Prefetch-Control: off
Expect-CT: max-age=0
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: no-referrer
X-XSS-Protection: 0
Content-Type: text/html; charset=utf-8
Content-Length: 12
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Date: Fri, 13 Nov 2020 15:08:04 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Hello World!

以上の新たに Helmet によって付加されたヘッダはセキュリティに関するヘッダであり、これらをレスポンスに設定することで様々な恩恵が得られます。

Helmet で対策される脆弱性

Helmet で追加されたヘッダーとそれにより対策される脆弱性を一覧化すると、次のようになります。
(ただし、必ずしもすべての対策がデフォルトで設定されるわけではありません)

ヘッダー 対策される脆弱性 Helmet API
Content-Security-Policy1 XSS攻撃 helmet.contentSecurityPolicy(options)
X-DNS-Prefetch-Control2 情報漏洩 helmet.dnsPrefetchControl(options)
Expect-CT3 不正なSSL証明書 helmet.expectCt(options)
X-Frame-Options4 クリックジャッキング攻撃 helmet.frameguard(options)
Strict-Transport-Security5 中間者攻撃 helmet.hsts(options)
X-Download-Options フィッシング helmet.ieNoOpen()
X-Content-Type-Options6 MIME スニッフィング helmet.noSniff()
X-Permitted-Cross-Domain-Policies7 XSS攻撃 helmet.permittedCrossDomainPolicies(options)
Referrer-Policy8 情報漏洩 helmet.referrerPolicy(options)
X-XSS-Protection9 XSS攻撃 helmet.xssFilter()

一つ一つ簡単に見ていきたいと思います。

Content-Security-Policy (CSP)

Content-Security-Policy: default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self'
https: 'unsafe-inline';upgrade-insecure-requests

これは主に XSS 攻撃を緩和する役目を果たします。
実行を許可するコンテンツのホワイトリストを作成し、その参照元のリソースだけを実行及びレンダリングするようにブラウザに指示します。
これにより、信頼できないコンテンツを事前に弾くことができ、不正なコードが注入されることを防ぎます。

Content-Security-Policy ではホワイトリストの一行一行をポリシーとして定義し、以下のようにセミコロン区切りで一つ一つ設定することになっています。

Content-Security-Policy: *policy*; *policy*; ...;

ポリシーについて一つ一つ見ていくと脱線するので、ここでは省略します。

X-DNS-Prefetch-Control

X-DNS-Prefetch-Control: off

ブラウザが事前にドメイン名の解決を実行する機能を制御します。ここでは DNS 先読みを無効にしています。MDN2には、

これはページのリンクを制御しない場合や、ドメインに情報漏洩させたくないと分かる場合に有用です。

とあり、Helmet の README でも

helmet.dnsPrefetchControl sets the X-DNS-Prefetch-Control header to help control DNS prefetching, which can improve user privacy at the expense of performance. See documentation on MDN for more.

とあるので、パフォーマンスを犠牲にしてでもユーザーのプライバシーを守るために off としているようですが、正直なぜ??という感じでいまいち理解できなかったのでもう少し調べてみました。
すると、同じような疑問を抱いていた方がほかにもいました。

私の理解の及ぶ限りで要約すると、DNS 先読みが有効であると、そのページで参照される全ての外部リソースに対してホスト解決のための DNS リクエストが発行されてしまい、それらが第三者によって盗聴される可能性があることを含めると、むやみにホスト解決を行うことは情報漏洩を招くことになる……ということだと思います。

Expect-CT

Expect-CT: max-age=0

不正な SSL 証明書を検知するための仕組みです。
以下の記事がお詳しいです。

デフォルトでは max-age0 になっており、特に何も設定していません。
ユーザが意図的に有効にする必要があるようです。

X-Frame-Options

X-Frame-Options: SAMEORIGIN

ブラウザがページを <frame>, <iframe>, <embed>, <object> の中に表示することを許可するか示すために使用されます。
CSP の frame-ancestors に似ており、実際 CSP で代替できますが、古いブラウザ向けにまだ有効のようです。
ここでは SAMEORIGIN を指定することで、ページ自体と同じオリジンのフレーム内でのみ表示できるようになります。

Strict-Transport-Security

Strict-Transport-Security: max-age=15552000; includeSubDomains

ウェブサイトがブラウザに対して HTTP の代わりに HTTPS を用いて通信を行うように指示します。
MDN5 の以下の解説がとても分かりやすいです。

もし、訪問者が http://www.foo.com/ または単に foo.com と入力したとき、ウェブサイトが接続を HTTP で受け付け、 HTTPS にリダイレクトするようになっていると、訪問者はリダイレクトされる前にまず、暗号化されないバージョンのサイトと通信する可能性があります。これは中間者攻撃の機会を作ってしまいます。リダイレクトは訪問者を、本来のサイトの安全なバージョンではなく、悪意のあるサイトに導くために利用される可能性があるからです。

HTTP の Strict Transport Security ヘッダーは、ブラウザーに対してサイトを HTTP を使用して読み込まず、サイトへのすべてのアクセスを、自動的に HTTP から HTTPS リクエストに変換するよう指示することができます。

ヘッダと直接関係はありませんが、リダイレクト自体、オープンリダイレクト脆弱性などを引き起こす可能性があるので注意したほうがよさそうです。

X-Download-Options

X-Download-Options: noopen

ブラウザ(IE8)がアプリケーションからの安全でない可能性のあるダウンロードを必ず保存させるようにします。
これにより、サイトのコンテキストで HTML の実行を抑止します。

X-Content-Type-Options

X-Content-Type-Options: nosniff

Content-Type ヘッダーで示された MIME タイプを変更せずに従うようにします。
これは MIME スニッフィング対策に有効です。

X-Permitted-Cross-Domain-Policies

X-Permitted-Cross-Domain-Policies: none

Adobe 製品に特有のクロスドメインポリシーファイル(crossdomain.xml)を許可するかどうかを指定します。
crossdomain.xml は同一ドメインポリシーによって制限されているドメイン間のデータを処理する許可を与えるポリシーを定義することができ、none に設定することですべてのポリシーファイルを不許可にしています。

Referrer-Policy

Referrer-Policy: no-referrer

リファラ情報をリクエストに含めないように設定しています。
これは情報漏洩を未然に防ぐのに有効でもあり、例えば URL にセッション ID などを埋め込んでいて悪意のある外部サイトへのリンクを踏んでしまうと、外部サイトへセッション ID が流出してしまい、セッションハイジャックを引き起こしてしまう可能性があります(徳丸本参照)。

X-XSS-Protection

X-XSS-Protection: 0

X-XSS-Protection による XSS フィルタリングを有効にすると、XSS 攻撃を検出したときにページの読み込みを停止します。
ここでは0にして XSS フィルタリングを無効化していますが、無効化にしている理由は以下の議論が参考になります。

ちゃんと理解できているか自信ないですが、X-XSS-Protection による XSS フィルタを利用することで、むしろ悪用されて XS-Leak 攻撃を引き起こす危険性があるため、無効にした方がいいらしい?

まとめ

今回調べてみて、DNS 先読みCertificate TransparencyX-XSS-Protection の悪用については全然知らなかったので、勉強になりました。

ただ、今回は表層を撫でてみただけで、「完全に理解した」には至っていないので、今回調べたことをもとに各種ヘッダーや Web の脆弱性そのものについてそれぞれ更に深く調べていきたいと思います。

余談

余談になりますが、先日ウェブ・セキュリティ試験(徳丸基礎試験)を受験して、無事に合格することができました。
この試験では今回見たようなウェブ・セキュリティの基礎的な知見を問うもので、とてもよい試験でした。試験については以下の記事がとても参考になります。

恥ずかしながら、今まで漠然とした不安の中で Web アプリケーションを開発していたのですが、この試験のために『体系的に学ぶ 安全なWebアプリケーションの作り方』(通称徳丸本)を何度か読み直して勉強したことで、少なからぬ根拠を持って実装に当たれるようになりました。

昨今ウェブ・セキュリティの需要が高まっている(と勝手に思っています)中、皆さんも年末年始楽しんで勉強してみてはいかがでしょうか。

出典

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

【DroidScript】スマホでスマホアプリを作る③ スピンボタンとシークバー

あらすじ

DroidScriptというアプリを使うと、javascriptベースの記述でスマホアプリが作れるらしいので、練習として「電子レンジワット数で加熱時間を換算するアプリ」を作る。

前回までのおさらい

アプリ画面の作成に着手。
Screenshot_20201121-154732.png
前回は画像の上から2行目まで作成。タイトルテキストとドロイド君を設置、ドロイド君をタップするとスマホが振動してアプリの説明がポップアップするようにした。

スマホでスマホアプリを作る① Hello World!
スマホでスマホアプリを作る② 背景やテキストの追加

スピンボタン(上下ボタン)の設置

txt = app.CreateText( "Your Microwave" ); 
txt.SetMargins( 0,0.02,0,0 ); 
txt.SetTextSize( 20 ); 
lay.AddChild( txt ); 

my = app.CreateSpinner( "730,700,680", 0.3 ); 
my.SetOnTouch( spin_my ); 
lay.AddChild( my ); 

txt = app.CreateText( "Label Microwave" ); 
txt.SetMargins( 0,0.02,0,0 ); 
txt.SetTextSize( 20 ); 
lay.AddChild( txt ); 

label = app.CreateSpinner( "500,1500", 0.3 ); 
label.SetOnTouch( spin_label ); 
lay.AddChild( label ); 

スピンボタンは2つ。自分のレンジのワット数と、温める対象のラベルに記載されたワット数。それぞれスピンボタンの前にテキストとマージンを挿入している。

app.CreateSpinner("選択肢,選択肢,・・・",幅);で設定。幅は画面幅を1.0として記述する。

自分のレンジは、自宅で使っている730Wと、職場の700W、たまに使う680Wを選択できるように設定。ラベルのワット数は500Wと1500Wを用意した。
Screenshot_20201121-164518.png
それぞれ、スピンボタンでワット数を選択した時の関数を作成し.SetOnTouch()で呼び出している。呼び出される関数がこちら。

function spin_my( item ) 
{ 
  app.ShowPopup( "Set " + item + "W" ); 
  app.Vibrate( "0,30" ); 
} 

function spin_label( item ) 
{ 
  app.ShowPopup( "Set " + item + "W" ); 
  app.Vibrate( "0,30" ); 
} 

選択したワット数が引数になる。例えば、700を選ぶとSet700Wとポップアップし、app.Vibrateで少し振動する。

シークバーの設置

txt = app.CreateText( "Label heating time" ); 
txt.SetMargins( 0,0.02,0,0 ); 
txt.SetTextSize( 20 ); 
lay.AddChild( txt ); 

skb = app.CreateSeekBar( 0.8 ); 
skb.SetOnTouch( skb_OnTouch ); 
skb.SetRange( 600 ); 
skb.SetValue( 180 ); 
lay.AddChild( skb ); 

app.CreateSeekBarでシークバーを作成。括弧内で画面に対する幅を設定。

.SetRangeでシークバーの最大値、.SetValueで初期値を設定している。ここで設定するのは電子レンジの加熱時間なので、最大値を600秒、初期値を180秒にしてみた。

シークバーを操作した時に呼び出される関数がこちら。

function skb_OnTouch( value ) 
{ 
  value = Math.round( value / 5 ) * 5; 
  var min = Math.floor( value / 60 ); 
  var sec = Math.floor( value % 60 ); 
  sec = ( "0" + sec ).slice( -2 ); 

  var settime = min + ":" + sec; 
  app.ShowPopup( "Set Time = " + settime ); 
  app.Vibrate( "0,10" ); 
} 

スピンボタンの時と同じように選択している秒数をポップアップさせるのに、受け取った数字をそのまま表示させるのではなく少し加工してみた。

1秒単位で細かく刻む必要はないので5秒単位に数字を丸め、m:ss表示ができるよう分と秒に分け、10秒未満でも2桁で表示するように加工した。

例)186 → 3:05
20201121_172920.gif

次回

計算実行ボタンを作成する。
スマホでスマホアプリを作る④ 実行ボタン

つづく

シリーズ

DroidScript電子レンジアプリ作成
スマホでスマホアプリを作る① Hello World!
スマホでスマホアプリを作る② 背景やテキストの追加
スマホでスマホアプリを作る③ スピンボタンとシークバー
スマホでスマホアプリを作る④ 実行ボタン
スマホでスマホアプリを作る シリーズまとめ

外伝
【GAS】電子レンジワット数で加熱時間を換算するwebアプリを作ってみた

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

Optional Chaining と Nullish Coalescing

Optional Chaining と Nullish Coalescing

ES2020, TypeScript 3.7 から導入された記法。
node では 13系からオプション指定をすることで (--harmony)、14系からは標準で使用できるようになっている。

記法

項目 記法
Optional Chaining ?.
Nullish Coalescing ??

Optional Chaining

挙動: プロパティを取るチェーンの途中でエラーを出さず、最終的に取れればそれを、取れなければ undefined を返す

例えば以下のコードにおいて

optionalChaining.js
const users = [
  {
    id: 1,
    name: 'sample user',
    career: {
      university: 'Hoge大',
    },
  },
  {
    id: 2,
    name: 'invalid user',
  },
]

users.forEach(user => console.log(user.name, user.career.university));

users[1]user.career.university にアクセスしようとすると

TypeError: Cannot read property 'university' of undefined

というエラーが起こる。
user.career までなら undefined が帰ってくるが、user.career.university までアクセスしようとすると怒られる。
これを何重に行っても怒られず、そのチェーン先プロパティが存在しない場合に、常に undefined を返してくれるようにするのが Optional Chaining である。
最後の行の .?. とするだけで、その挙動になる。

optionalChaining.js
users.forEach(user => console.log(user.name, user?.career?.university));
// 実行結果
// sample user Hoge大
// invalid user undefined

Nullish Coalescing

挙動: nullundefined の場合のみ右側を評価し、そうでない場合は左側の被演算子を返す

例えば以下のようなコードがあったとする。

nullishCoalescing.js
const user = {
  id: 1,
  name: 'sample user',
  career: {
    university: 'Hoge大',
    high_school: null,
  },
}

console.log(user.career.high_school || '高校登録なし')

ただし、||falsy な値すべてを false 扱いしてしまう。

false
""
0
undefined
null

そうではなく、純粋に左側の被演算子が null (存在しないという意味) か undefined (定義されていない) の場合に、右側を評価したい場合 Nullish Coalescing が有効である。
そしてそれは先ほどのコードの ||?? に書き換えるだけで完了する。

nullishCoalescing.js
const user = {
  id: 1,
  name: 'sample user',
  career: {
    university: 'Hoge大',
    high_school: null,
  },
}

console.log(user.career.high_school ?? '高校登録なし')
// => 高校登録なし

結論

今後ネストしたプロパティにアクセスするときは Optional Chaining
null または undefined という判定をしたいなら Nullish Coalescing
を使うと、短く簡潔にかける。

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

【DroidScript】スマホでスマホアプリを作る② 背景やテキストの追加

あらすじ

DroidScriptというアプリを使うと、javascriptベースの記述でスマホアプリが作れるらしいので、練習として「電子レンジワット数で加熱時間を換算するアプリ」を作る。

前回のおさらい

ボタンをタップすると「Hollo World!」と表示されるサンプルアプリのコードを見た。
スマホでスマホアプリを作る① Hello World!

どうやらlayにパーツを放り込むことでアプリの画面をレイアウトできそう。

function OnStart()
{   
  lay = app.CreateLayout( "linear", "VCenter,FillXY" ); 
  app.AddLayout( lay );
}

と言うわけで、自作アプリに向けてレイアウトを作成していく。

まずは完成画面

絶望的な英語力や配置センスは気にしないでいただきたい(ToT)
Screenshot_20201121-154732.png
上から
・タイトル
・ドロイド君
・自分のレンジのワット数
・温める対象のラベルに記載されたワット数
・温める対象のラベルに記載された加熱時間
・計算ボタン
となっている。

背景とタイトル

lay = app.CreateLayout( "linear", "VCenter,FillXY" );
lay.SetBackground( "/Sys/Img/BlueBack.jpg" );

txt = app.CreateText( "HOW MANY MIN" );
txt.SetTextSize( 22 );
lay.AddChild( txt );

1行目でlayの基本設定。括弧内の記述はサンプルコードのコピペなのでよくわからん^^;

lay.SetBackgroudで背景を設定。最初から用意されているBlueBack.jpgを使用。

タイトルの「HOW MANY MIN」というテキストを作りAddChildした。

ドロイド君

img = app.CreateImage( "/Sys/Img/Icon.png", 0.1 );
img.SetMargins( 0, 0.02, 0, 0 );
img.SetOnTouchDown( img_OnTouchDown );
lay.AddChild( img );

こちらも最初から用意されているドロイド君のアイコンIcon.pngをAddChild。.SetMarginsで、その前にAddChildしたパーツとの間に間隔を設ける。.SetOnTouchDownで、パーツをタップした時に呼び出す関数img_OnTouchDownを設定した。

ドロイド君をタップしたら呼び出される関数がこちら

function img_OnTouchDown( ev )
{
  app.ShowPopup( "I will calculate the heating time!" );
  app.Vibrate( "0,100,30,100,50,300" );
}

何のアプリか簡単な説明をポップアップさせ、app.Vibrateでスマホを振動させる。

app.Vibrateの括弧内は、("休止,振動,休止,振動,・・・")で、数字はミリ秒となっているみたい。

次回

ワット数や加熱時間を入力する部分を作成する。
スマホでスマホアプリを作る③ スピンボタンとシークバー

つづく

シリーズ

DroidScript電子レンジアプリ作成
スマホでスマホアプリを作る① Hello World!
スマホでスマホアプリを作る② 背景やテキストの追加
スマホでスマホアプリを作る③ スピンボタンとシークバー
スマホでスマホアプリを作る④ 実行ボタン
スマホでスマホアプリを作る シリーズまとめ

外伝
【GAS】電子レンジワット数で加熱時間を換算するwebアプリを作ってみた

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

【Electron入門】GithubのIssueを一瞬で確認できるアプリを作って業務効率化してみた #2 GithubAPI編

はじめに

前回はwindowの作成をしました。
今回はGithubAPIを導入して完成させようと思います。
長くなってしまうのでJavaScriptでDOMの作成をする部分やCSSの説明は省きます。
気になる方はコードをご確認ください!

完成品

option(alt) + Spaceで表示/非表示が切り替えられます。

scc.gif

現状

test.gif

Access Tokenを生成する

それではやっていきましょう。

Githubにログインし、
Settings > Developer settings > Personal access tokens
と進みます。

image.png

Generate new tokenを押すとtoken作成画面に進めます。

Noteにはtokenを識別するために用途を書きます。自分はgit-electronとしました。

Select scopesでtokenの権限を選択できます。今回はrepoの権限全てにチェックをつけます。

Generate tokenを押すとAccess Tokenが生成されるので、
後ほど使うので、赤く囲まれている部分のAccess Tokenをコピーしておきます。

スクリーンショット 2020-11-21 12.13.34.png

AccessTokenを使ってAPIにアクセスする

実際にGithubAPIを叩いてみます。

通信にはaxiosを使用しました。
Tokenの管理にはdotenvを使用しました。

1 . アプリのルートパスで.envファイルを作成して以下のように編集します。

.env
GH_ACCESS_TOKEN=[先ほどコピーしたAccess Token]
GH_BASE_URL=https://api.github.com

2 . main.jsに以下を追記して.envファイルを読み込みます。

main.js
require("dotenv").config({ path: __dirname + "/.env" });

※ AccessTokenは外部に見られたくないので、Githubで公開する際には.gitignoreを作成してremoteに送らないようにしましょう。

.gitignore
.env



3 . axiosを拡張する

axiosの設定を共通化するために拡張します。
自分はpluginsディレクトリを作成して、配下にaxios.jsを作成しました。

plugins/axios.js
const axiosBase = require("axios");

const axios = axiosBase.create({
  baseURL: process.env.GH_BASE_URL,
  headers: {
    Authorization: "Bearer " + process.env.GH_ACCESS_TOKEN,
    "Content-Type": "application/json",
  },
});
module.exports = axios;

これ以降、axiosを呼び出すときはこのaxios.jsを呼び出します。

これでGithub APIを呼び出す準備は整いました!

フロント(Window)からAPIを呼び出す

Electronにはメインプロセスレンダラープロセスがあります。

メインプロセス内(ここで言うmain.js)でBroeserWindowインスタンスを作成し、そのBroeserWindowインスタンスがレンダラープロセス内でWebページ(html)を表示します。
公式ドキュメント

よってhtmlから呼び出したJavaScriptなどはレンダラープロセスで実行する事になります。

ここで困ることが一点あります。
main.jsはメインプロセス上で動くので、Node.jsの文法が使えますが、レンダラープロセスでは使えません。

requireなどNode.js上で使えるメソッドをフロントからの要求に応じて使えるようにするには、
contextBridgeを使って、メイン⇔プロセス間の通信をセキュアにする必要があります。


詳しく知りたい方は以下の記事がうまくまとまっていると思うので参考にしてみてください!

ElectronでcontextBridgeによる安全なIPC通信 - Qiita
ElectronでcontextBridgeによる安全なIPC通信(受信編) -Qiita

contextBridgeを実装する

contextBridgeを使用する為のpreload.jsをルートディレクトリに作成します。

preload.js
const { contextBridge } = require("electron");

contextBridge.exposeInMainWorld("api", {
  github: {
    getIssues: () => {
      // メインプロセスで行いたい処理を書く
      const axios = require("./plugins/axios");
      axios
        .get("/issues")
        .then((res) => {
          console.log(res);
        })
        .catch((e) => console.log(e));
    },
  },
});

次にmain.jsを修正して、BrowserWindowでpreload.jsを読み込むよう設定します。

main.js
  const win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      preload: __dirname + "/preload.js",
    },
    // .....省略......
  })

最後にindex.htmlにscriptタグと発火用のボタンを追加して、preload.jsで定義した関数を呼び出せるようにします。

index.html
  <body style="-webkit-app-region: drag">
    <div style="background: #fff; height: 300px">
      <h1>Hello World!</h1>
      <button onclick="fetchAPI()">get issues</button>
    </div>
  </body>
  <script>
    function fetchAPI() {
      window.api.github.getIssues();
    }
  </script>

ボタンを押して、Electron上でDeveloper toolを開くとIssueのデータが取れていると思います!

image.png

デスクトップアプリとして使いたい場合は、npm run buildでelectron-builderが起動し、dist配下にアプリが生成されます。

DOMの生成やCSSはリポジトリを参考にしてみてください!

最後に

今回はElectroとGithubAPIを組み合わせて業務効率化アプリを作ってみました。

かなり手軽にデスクトップアプリを作成することができた一方、いくつか制限もあったので大規模開発での導入には検討が必要かもしれません。(と言ってもVSCodeなどもelectronで開発されているのである程度安心していいと思っています)

こんな感じで週1~2でアウトプットを記事にしていこうと思います。是非LGTM、フォローの方お願い致します!

Twitterのフォローもお願い致します!
https://twitter.com/1keiuu

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

【Electron入門】GithubのIssueを一瞬で確認できるアプリを作って業務効率化してみた #1 Alfredっぽいwindow作成編

完成品

option(alt) + Spaceで表示/非表示が切り替えられます。

scc.gif

ソースコード

https://github.com/ikkei12/git-app.pub

はじめに

image.png

Alfred最高ですよね。
彼のおかげでマウスを使う頻度が激減して助かっています。

Alfredと同じ手軽さで、自分がアサインされているissueを確認できるデスクトップアプリが作りたいなと思ったので、Electronで作ってみようと思いました。

Electronには

  • JavaScriptで作れる
  • Chromiumベースなのでwebエンジニアにも優しい

といった利点があり、HTML/CSS/JavaScriptを学んだ初心者の方でも扱いやすいかと思います!

Electronのはじめかた

基本的に公式ドキュメントを参考にElectronのセットアップをするだけですが、一部変更を加えています。

1. 以下のコマンドを実行

mkdir git-app && cd git-app
npm init -y
npm i --save-dev electron
npm i --save-dev electron-builder

2. html/jsファイルを作成

標準的なNode.jsアプリと同様に以下のような構成で始めます。

git-app/
├── package.json
├── main.js
└── index.html
main.js
const { app, BrowserWindow } = require('electron')

function createWindow () {
  const win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
    },
    frame: false,
    alwaysOnTop: true,
    useContentSize: true,
    transparent: true,
  })

  win.loadFile('index.html')

  win.on("blur", (e) => {
    // window外をクリックしたタイミング(blur)で閉じる
    app.hide();
  });
}

app.whenReady().then(createWindow)

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})
index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World!</title>
    <meta
      http-equiv="Content-Security-Policy"
      content="script-src 'self' 'unsafe-inline';"
    />
  </head>
  <body style="-webkit-app-region: drag">
    <div style="background: #fff; height: 300px">
      <h1>Hello World!</h1>
    </div>
  </body>
</html>

3. package.jsonを修正

package.jsonは以下のように書き換えます。

package.json
{
  "name": "git-app",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "NODE_ENV=development electron .",
    "build": "NODE_ENV=production electron-builder"
  },
  "build": {
    "appId": "git-app",
    "mac": {
      "category": "git-app"
    }
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "electron": "^10.1.5"
  }
}

4. 起動

npm start
でこのようなwindowが表示されると思います。

image.png

どこからでもショートカットでアプリを呼び出せるようにする

このままではwindowの表示/非表示を切り替えられないし、PCの起動時に毎回アプリを立ち上げる必要があります。

Alfredのような挙動を実現するには
 1. electronアプリを常駐で起動させておいて、
 2. 表示/非表示を切り替えるショートカットを登録する
必要があります。

1. electronアプリを常駐で起動させる

main.js
app.dock.hide();

if (process.env.NODE_ENV !== "development") {
  app.setLoginItemSettings({
    openAtLogin: true,
    path: app.getPath("exe"),
  });
}

解説

app.dock.hide()
常駐のアプリをDockに表示したくないので、Dockから非表示にします。

app.setLoginItemSettings()
でログイン時に自動で起動してくれるようになります。
ローカルでは起動したくないので、NODE_ENVで判定しています。

・macではログイン時に起動されるアプリは
ユーザーとグループ > ログイン項目に追加されます。
image.png

2. 表示/非表示を切り替えるショートカットを登録する

main.js
app.on("ready", function () {
  globalShortcut.register("alt+space", function () {
    // 現在focusしているwindowを取得
    const window = BrowserWindow.getFocusedWindow();
    // windowが存在すればhide、なければshow
    window ? hideWindow(window) : showWindow();
  });
});

app.on("will-quit", function () {
  // 終了するタイミングで全てのglobalShortcutを解除
  globalShortcut.unregisterAll();
});

function showWindow() {
  // focusさせる事でBrowserWindowのblurイベントを検知させる
  app.focus({ steal: true });
  app.show();
}

function hideWindow(window) {
  // center()する事でshowする時に中央で表示される
  window.center();
  app.hide();
}

解説

・アプリがready状態になった段階で、ショートカットキーを登録しています。自分はalt+spaceで起動したいのでregisterの第一引数に渡しています。

・第二引数ではショートカットキーが押されたタイミングで実行する関数を登録しています。現在focusされているwindowを取得し、存在する場合はhideメソッド、しなければshowメソッドを実行する事で表示を切り替えています。

globalShortcutと言うのがポイントで、普通のショートカット(localShortcut)ではElectronアプリがフォアグラウンドになっている場合にしか反応しません。


このように動くと思います!

test.gif

Tips

Windowをドラッグできるようにする

<body style="-webkit-app-region: drag">

参考: https://www.electronjs.org/docs/api/frameless-window#draggable-region

まとめ

ご覧いただきありがとうございました!
次回はGithub APIからIssueを取得して表示する部分を作成していきます!

#2 GithubAPI編

Twitterのフォローもお願い致します!
https://twitter.com/1keiuu

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

【DroidScript】スマホでスマホアプリを作る①

概要

DroidScriptと言うスマホアプリがある。

javascriptを記述して、オリジナルアプリが作れてしまうらしい。しかもスマホで(そりゃあスマホアプリなんだからそうなんだろうけど何か衝撃)。

GASを覚えたことで、javascriptなら行けんじゃね?と、ちょっと触ってみることにした。

DroidScript

Playストアから入手可能。

サンプルコードが大量にあるためヒントは多いけど、コードの記述の方法とか、専用のメソッドを解説しているサイトが少なくて手探りになりそう・・・。

サンプルコードを見ると、横スクロールのゲームやらメディアプレイヤーやらある他、カメラ、メール、USBなどなど、出来ることがかなり多い。

アプリの画面をPCに表示させる方法もあって、スマホでコチコチやらなくてもコード入力できるみたい。

基本操作は全て無料!プラグインが豊富で、APKファイルへのビルドやブルートゥース対応など、一部のプラグインが有料になっている。

目的

以前に、WEBアプリ導入の練習で「電子レンジワット数で加熱時間を換算するアプリ」を作ったので、今回も同じお題で練習してみる。

参照:【GAS】電子レンジワット数で加熱時間を換算するwebアプリを作ってみた

Hello World!

何事もハローワールド!から。

アプリをインストールしたところ、最初にサンプルアプリが用意されていたので起動してみた。
Screenshot_20201121-123800.png
画面にはドロイド君と「Press Me」と書かれたボタンが1つ(左画像)。ボタンをタップすると「Hello World!」と画面に表示された(右画像)。

コードを表示させたところこんな感じ。

//Called when application is started.
function OnStart()
{   
  //Create a layout with objects vertically centered.
  lay = app.CreateLayout( "linear", "VCenter,FillXY" ); 

  //Create image 1/5 of screen width and correct aspect ratio.
  img = app.CreateImage( "Img/Hello World.png", 0.2, -1 );
  lay.AddChild( img );      

  //Create a button 1/3 of screen width and 1/10 screen height.
  btn = app.CreateButton( "Press Me", 0.3, 0.1 );
  btn.SetMargins( 0, 0.05, 0, 0 );
  lay.AddChild( btn );

  //Set function to call when button pressed.
  btn.SetOnTouch( btn_OnTouch );

  //Add layout to app.
  app.AddLayout( lay );
}

//Called when user touches our button.
function btn_OnTouch()
{   
  //Show a popup message.
  app.ShowPopup( "Hello World!" );
}

英語は読めないけど、どうやらOnStart()がアプリを開いたときに呼び出される関数みたい。

app.CreateLayoutで宣言したlay.AddChildで表示させたいアイテムを放り込み、app.AddLayoutでアプリに渡してるんだなぁ・・・とか、

btn.SetOnTouch( btn_OnTouch )でボタンがタップされた時の関数を呼び出してるみたいだなぁ・・・とか、

function btn_OnTouch()では、app.ShowPopupでメッセージがポップアップするんだろうなぁ・・・など、

何とか分かりそう。

次回

自作アプリの初期画面を作ってみる。

つづく

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

【DroidScript】スマホでスマホアプリを作る① Hello World!

概要

DroidScriptと言うスマホアプリがある。

javascriptを記述して、オリジナルアプリを開発できるらしい。しかもスマホで!そりゃあスマホアプリなんだからそうなんだろうけど何か衝撃。

GASを覚えたことで、javascriptなら行けんじゃね?と、ちょっと触ってみることにした。

DroidScript

Playストアから入手可能。

サンプルコードが大量にあるためヒントは多いけど、ググっても日本語のサイトはあまりヒットしないし、Qiitaの記事もゼロ・・・操作は手探りになりそう(´・ω・`)

サンプルコードを見ると、横スクロールのゲームやらメディアプレイヤーやらある他、カメラ、メール、USBなどなど、出来ることはかなり多い。

アプリの画面をPCに表示させる方法もあって、スマホでチマチマやらなくてもキーボードでコード入力できるみたい(ちな、今回は全てスマホから操作)。

基本操作は全て無料!プラグインが豊富で、APKファイルへのビルドやブルートゥース対応など、一部のプラグインが有料になっている。

目的

以前に、WEBアプリ導入の練習で「電子レンジワット数で加熱時間を換算するアプリ」を作ったので、今回も同じお題で練習してみる。

参照:【GAS】電子レンジワット数で加熱時間を換算するwebアプリを作ってみた

Hello World!

何事もハローワールド!から。

アプリをインストールしたところ、最初にサンプルアプリが用意されていたので起動してみた。
Screenshot_20201121-123800.png
画面にはドロイド君と「Press Me」と書かれたボタンが1つ(左画像)。ボタンをタップすると「Hello World!」と画面に表示された(右画像)。

コードを表示させたところこんな感じ。

//Called when application is started.
function OnStart()
{   
  //Create a layout with objects vertically centered.
  lay = app.CreateLayout( "linear", "VCenter,FillXY" ); 

  //Create image 1/5 of screen width and correct aspect ratio.
  img = app.CreateImage( "Img/Hello World.png", 0.2, -1 );
  lay.AddChild( img );      

  //Create a button 1/3 of screen width and 1/10 screen height.
  btn = app.CreateButton( "Press Me", 0.3, 0.1 );
  btn.SetMargins( 0, 0.05, 0, 0 );
  lay.AddChild( btn );

  //Set function to call when button pressed.
  btn.SetOnTouch( btn_OnTouch );

  //Add layout to app.
  app.AddLayout( lay );
}

//Called when user touches our button.
function btn_OnTouch()
{   
  //Show a popup message.
  app.ShowPopup( "Hello World!" );
}

英語は読めないけど、どうやらOnStart()がアプリを開いたときに呼び出される関数みたい。

app.CreateLayoutで宣言したlay.AddChildで表示させたいアイテムを放り込み、app.AddLayoutでアプリに渡してるんだなぁ・・・とか、

btn.SetOnTouch( btn_OnTouch )でボタンがタップされた時の関数を呼び出してるみたいだなぁ・・・とか、

function btn_OnTouch()では、app.ShowPopupでメッセージがポップアップするんだろうなぁ・・・など、

何とか分かりそう。

次回

次回から自作アプリの作成。
スマホでスマホアプリを作る② 背景やテキストの追加

つづく

シリーズ

DroidScript電子レンジアプリ作成
スマホでスマホアプリを作る① Hello World!
スマホでスマホアプリを作る② 背景やテキストの追加
スマホでスマホアプリを作る③ スピンボタンとシークバー
スマホでスマホアプリを作る④ 実行ボタン
スマホでスマホアプリを作る シリーズまとめ

外伝
【GAS】電子レンジワット数で加熱時間を換算するwebアプリを作ってみた

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

1ファイルのHTML(+JavaScript)で、QRcodeを生成する

1ファイルのHTML(+JavaScript)で、QRcodeを生成する

・ライブラリ等を利用せず、javascriptを持つ1ファイルのHTML文書で、QRコードを生成できる
・javascript内で、スクラッチ(ゼロ)からQRcodeを書き出している
・型番は1~25まで対応
・最大718byteまでデータ容量を持てる。日本語(UTF-8)で、約230文字ほど
・訂正レベル「Q」、マスクパターンは「011」で固定

外観

(以下↓はイメージ(画像)です)
QRcode_javascript.png

動作サンプル

http://thomas.cranky.jp/js_QR/QRcode_javascript.html

github

https://github.com/santarou6/QRcode_javascript/blob/main/QRcode_javascript.html


参考

QRコードの8種類のマスクを図示する
https://qiita.com/santarou6/items/f254f9fa5ea32d83ae27

エクセルの、ワークシート関数のみでQRコードを作成する
https://qiita.com/santarou6/items/dda2f88f42c55cd2118f

EXCELのVBAだけでQRコード。一部修正して日本語(全角)でも作成可能に。サンプルのエクセルファイルあり。
https://qiita.com/santarou6/items/d623417ea8ba33756108

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

【Vue.js】Vue.jsとcssアニメーションで作るスライドショー

Vue.jsCSSを用いてスライドショーを実装するメモです。

目的

スライド画像.gif
上記のようなシンプルなスライドショーを実装します。

実装

テンプレート

Imageslide.vue
<template>
  <div>
    <transition-group tag="ul" class="images">
      <li
        v-for="(image, index) in demoimages"
        v-show="currentImage == index + 1"
        :key="index"
      >
        <img :src="image.image" alt="" />
      </li>
    </transition-group>
  </div>
</template>

画像をv-forでリストしてv-showで表示を切り替えています。
v-showv-ifとは違い、DOMを消したり追加したりはせず、display:noneを付与して要素を非表示にします。そのため非表示の画像をもう一度表示する場合も、再度画像が読み込まれるわけではありません。

スクリプト

Imageslide.vue
<script>
export default {
  data() {
    return {
      currentImage: 1,
    }
  },
  computed: {
    demoimages() {
      return [
        { image: '/images/1.jpg' },
        { image: '/images/2.jpg' },
        { image: '/images/3.jpg' },
      ]
    },
  },
  watch: {
    currentImage() {
      this.autoSlide()
    },
  },
  mounted() {
    this.autoSlide()
  },
  methods: {
    async autoSlide() {
      const wait = (ms) =>
        new Promise((resolve) => setTimeout(() => resolve(), ms))
      await wait(2000)
      if (this.currentImage > this.images.length - 1) {
        this.currentImage = 1
      } else {
        this.currentImage++
      }
    },
  },
}
</script>

表示されている画像のインデックスを示すcurrentImageをウォッチしてautoSlide()を実行しています。currentImageが画像のリストを超えた場合に初期化することで無限ループになります。

スタイル

Imageslide.vue
<style lang="scss" scoped>
.images {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  position: relative;
  li {
    width: 100%;
    height: 100%;
    max-height: 627px;
    position: absolute;
    top: 0;
    left: 0;
  }
}
img {
  width: 100%;
  height: 100%;
  max-height: 627px;
  margin: auto;
  z-index: 2;
}
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s;
}

.v-enter,
.v-leave-to {
  opacity: 0;
}
</style>

リストの親要素にposition:relativeを指定し、子要素にposition:absolute top:0 left:0を指定することで同じ位置に画像を重ねることができます。
.v-enter-active,.v-leave-active,.v-enter,.v-leave-totransitionによってVueから自動的に付与されるクラスです。

まとめ

Vue.jsとCSSを使用したスライドショーの実装方法について簡単に説明してみました。私はこのように実装しましたが、おそらく別の方法もあるかと思います。いろいろ試してみてください!

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

v-bind:keyは並び替えるためのものじゃない

結論

v-bind:keyはそれぞれの要素に一意なキーを与えるだけ。並べ替えはしません。
並べ替えるなら関数使って新しく作りましょう。

お恥ずかしいことに、下記のようにキーに数字を与えると自動で並べ替えられると思っていました。

<span v-for="num in [3, 2, 1]" :key="num">
    {{ num }}
</span>

<!-- 期待したのは出力はこっち--> 
<!-- 1 2 3 -->

<!-- 実際の出力は以下になる -->
<!-- 3 2 1 -->

具体的な並び替え方は以下が参考になりました。
Vue.jsのv-forでの並び順を指定したキーの値順にしたり、昇順、降順を切り替えたりする

まとめ

keyは一意に定めるものでそれ単体で並び替えに使えるものではありません。並び替えるならcomputedプロパティなどを使って並び替えたデータを用意しましょう。

私のように変な勘違いを起こした方が路頭に迷わないためにここに残しておきます。

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

途中から「fixed」で、上スクロールでふわっと降りてきて、下スクロールで消えるトップメニュー

上スクロールでふわっと降りてきて、下スクロールで消えるトップメニュー

を作ってみました。

See the Pen Topmenu that appear-scrollup & hide-scrolldown by sarap422 (@sarap422) on CodePen.

よくあるやり方と若干違うのは、
最初から position: fixed; でなく、
スクロール量に応じて、途中から position: fixed; にしています。

つまり、どういうことかと言いますと、
スマホのときに、最初のトップメニューはそのまま使いたい
途中から、上スクロールで下タブ表示されるようにしたい
という動きになります。

あと最初が position: static;(初期値) のままだから、
レイアウト調整が楽とかもあるかもしれないですね。

解説部分

scroll量計算・グローバル変数

javascript

$(function () {
// グローバル変数
previous_scrY = 0;

// window 'scroll' があったとき
$(window).on("scroll", function () {
// scroll量計算
let current_scrY = window.pageYOffset;
let diff_scrY = current_scrY - previous_scrY;

current_scrY(現在スクロール量) - previous_scrY(前回スクロール量) で、上スクロールか下スクロールかと判定します。
「var」や「let」だと上手く行かなかったのですが、「グローバル変数」というのがあるらしいですね。これだと上手くいきました。

「position:fixed」に変更、「.hide」クラスで一旦隠す

javascript

// 現scrollY > 150 (※「pos:fixed」に変更、「.hide」で一旦隠す)
if (current_scrY > 150) {
    // 現scrollYが、前scrollYより大きい場合
    if (diff_scrY > 0) {
        // id '#' に、class'rolldown-hide'を追加
        $("#Meltlilith").removeClass("rollup-PC rollup-Mbl");
        $("#Meltlilith").addClass("rolldown-hide-PC rolldown-hide-Mbl");
    }
    // 現scrollYが、前scrollYより小さい場合
    if (diff_scrY < 0) {
        // id '#' に、class'rollup-hide'を追加
        $("#Meltlilith").removeClass("rolldown-PC rolldown-Mbl");
        $("#Meltlilith").addClass("rollup-hide-PC rollup-hide-Mbl");
    }

css

@media only screen and (max-width: 500px) {
.rollup-Mbl .toplogo, .rollup-Mbl .toplogo {
    display: none;
}
.rolldown-Mbl, .rolldown-hide-Mbl {
    position: fixed;
    z-index: 999;
    width: 100%;
    justify-content: center;
    bottom: -100%;
}

途中で、position: fixed; に変更する場合
cssの transition でふわっとならずに、すぐにドンと表示されたんですね。

それとスマホのときの、「最初は上表示 → 途中は下から表示」も上手く行かなかったので、一旦隠す処理を入れました。

隠していた「fixed」メニューを表示

javascript

// 現scrollY > 200
if (current_scrY > 200) {
    // 現scrollYが、前scrollYより大きい場合
    if (diff_scrY > 0) {
        // id '#' を、class'rolldown-hide' → 'rolldown'に移行
        $("#Meltlilith").removeClass("rolldown-hide-PC rolldown-hide-Mbl");
        $("#Meltlilith").addClass("rolldown-PC rolldown-Mbl");
    }
    // 現scrollYが、前scrollYより小さい場合
    if (diff_scrY < 0) {
        // id '#' を、class'rollup-hide' → 'rollup'に移行
        $("#Meltlilith").removeClass("rollup-hide-PC rollup-hide-Mbl");
        $("#Meltlilith").addClass("rollup-PC rollup-Mbl");
    }
} /*ƒ /if (current_scrY > 200) */

css

.rollup-Mbl {
    position: fixed;
    z-index: 999;
    width: 100%;
    justify-content: center;
    bottom: 0;
}

あとは「.roll-hide」クラスを削除して、「.roll」で表示というだいたい同じような作りですね。

最後に「現在のスクロール量」を次回スクロール時の比較値として記録

javascript

} else {
    /* not(current_scrY > 150) */
    // "roll"classを全削除
    $("#Meltlilith").removeClass(
        "rolldown-hide-PC rollup-PC rolldown-hide-PC rolldown-PC rolldown-hide-Mbl rollup-Mbl rolldown-hide-Mbl rolldown-Mbl"
    );
}

// 処理後に、現scrollYを、前scrollYとして記録
previous_scrY = current_scrY;

上に来たときは、 position: static;(初期値) に戻るよう「.roll」クラスを全削除、
処理が終わった最後に、現在のスクロール量を、次回比較値として記録します。

下タブ時はアイコンも表示されるようにしてみる

css

.rollup-Mbl [id^='Top__li'] i:before {
    font-family: 'Font Awesome 5 Free';
    font-weight: 900;
    font-style: normal;
    font-size: 1.5rem;
    /* content内での改行ありにする */
    white-space: pre;
}
.rollup-Mbl #Top__li-Home i:before {
    content: '\f015\a';
}
.rollup-Mbl #Top__li-Works i:before {
    content: '\f0b1\a';
}
.rollup-Mbl #Top__li-About i:before {
    content: '\f007\a';
}

疑似要素で、下タブ時はアイコンも表示されるようにもしてみました。
white-space: pre;content: '\a'; でアイコン改行です。

まとめ

ちなみにふわっと降りてくるだけなら、もう少しシンプルに。
既存のトップメニューのままでも、id付け加えるだけでもある程度は動くんじゃないでしょうか? 以上です。

css

/* scrollPeraFix-falling.js
  (上スクロールでTopbar表示、下スクロールで消去)
=================================================================== */

#Meltlilith {
    width: 100%;
    transition: all 0.3s ease-in-out;
}

.rolldown, .rollup-hide {
    position: fixed;
    z-index: 999;
    width: 100%;
    top: 0;
}

.rollup {
    position: fixed;
    z-index: 999;
    width: 100%;
    top: -100%;
}

javascript

/* scrollPeraFix-falling.js
  (上スクロールでTopbar表示、下スクロールで消去)
=================================================================== */

$(function () {
  // グローバル変数
  previous_scrY = 0;

  // window 'scroll' があったとき
  $(window).on("scroll", function () {
    // scroll量計算
    let current_scrY = window.pageYOffset;
    let diff_scrY = current_scrY - previous_scrY;

    // 現scrollY > 150 (※「pos:fixed」に変更、「.hide」で一旦隠す)
    if (current_scrY > 150) {
      // 現scrollYが、前scrollYより大きい場合
      if (diff_scrY > 0) {
        // id '#' に、class'rolldown-hide'を追加
        $("#Meltlilith").removeClass("rollup");
        $("#Meltlilith").addClass("rolldown-hide");
      }
      // 現scrollYが、前scrollYより小さい場合
      if (diff_scrY < 0) {
        // id '#' に、class'rollup-hide'を追加
        $("#Meltlilith").removeClass("rolldown");
        $("#Meltlilith").addClass("rollup-hide");
      }

      // 現scrollY > 200
      if (current_scrY > 200) {
        // 現scrollYが、前scrollYより大きい場合
        if (diff_scrY > 0) {
          // id '#' を、class'rolldown-hide' → 'rolldown'に移行
          $("#Meltlilith").removeClass("rolldown-hide");
          $("#Meltlilith").addClass("rolldown");
        }
        // 現scrollYが、前scrollYより小さい場合
        if (diff_scrY < 0) {
          // id '#' を、class'rollup-hide' → 'rollup'に移行
          $("#Meltlilith").removeClass("rollup-hide");
          $("#Meltlilith").addClass("rollup");
        }
      } /*ƒ /if (current_scrY > 200) */
    } else {
      /* not(current_scrY > 150) */
      // "roll"classを全削除
      $("#Meltlilith").removeClass(
        "rollup-hide rollup rolldown-hide rolldown"
      );
    }

    // 処理後に、現scrollYを、前scrollYとして記録
    previous_scrY = current_scrY;
  });
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[PlayCanvas] Button Elementには気をつけよう

なにがあったのか

Button ElementにはHover, Pressed, InactiveのTintが設定できるようになっています。
その中でもInactiveButton ComponentActiveのチェックボックスが外れていると適応されるTintで、文字通り「非活性状態」のボタンを表現できます。
これを使用して「条件が満たされるまで押せないボタン」を実装しようとしましたが動画の通り。
demo_1.gif
非活性にしているはずのボタン(Black)をクリックしたらイベントが発火しました。

正直に申し上げると、Unityの感覚が抜けていなかったので混乱しました。Activeのチェック外してるのになんで処理が走るのかと。

なので公式リファレンスのactiveの項目を確認したら、このように書いてあった。

If set to false, the button will be visible but will not respond to hover or touch interactions.
訳) falseに設定した場合、ボタンは表示されますが、ホバーやタッチのインタラクションには反応しません。

イベントの発火について触れられてなかったorz
あくまでこのActiveのチェックボックスはボタンのビジュアルに影響を与えるだけで、イベントの発火には関わっていないとの事でした。

対処

押された時の処理部分を下記の様に修正しました。

var Button = pc.createScript('button');

Button.prototype.onPress = function() {
    /* ボタンコンポーネントの active が true なら実行 */
    if( this.entity.button && this.entity.button.active ){
        /* 処理内容 */
    }
};

非常にシンプルですが、Button Componentactivetrueになってる時だけ処理するようにしました。
demo_2.gif
結果はこの通り、非活性状態のボタン(Black)をクリックしても動作しなくなりました。

おわりに

これも以前の記事同様、Unityライクに書けるがゆえに起きたミスではあるんですが、正直予想外でした。
入力イベントの登録方法を見た時に気がつくべきではあったのですが…

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

関数の呼び出し[Javascript]

Javascript ~関数の呼び出し~

関数を定義しただけでは、その中の処理は実行されない。図のように、関数を定義した際に使用した定数名を用いて、「定数名()」と書くことで関数の中の処理を実行できる
46e36760d965e0fe3fadf99676883857.png
6c9e37b2b045ca7f070adb4c6ae2ae5e.png

引数を受け取る関数の定義

(引数名) =>」と括弧の中に引数名を書くことで引数を受け取ることができる
1529043476847.png
1529044955793.png

以上!

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