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

self bookmark's

ディザスタリカバリ

災害などによる被害からの回復措置、あるいは被害を最小限に抑えるための予防措置のこと。
http://ja.wikipedia.org/wiki/%E3%83%87%E3%82%A3%E3%82%B6%E3%82%B9%E3%82%BF%E3%83%AA%E3%82%AB%E3%83%90%E3%83%AA

リンクリンク

無料のSSL証明書

https://letsencrypt.org/
https://www.cloudflare.com/

OpenFlow

仮想のネットワーク機器を定義出来る。opensourceでモジュール構成に OSGiを使っている
https://www.nic.ad.jp/ja/newsletter/No52/0800.html

企業コード(問題点・官邸)

http://www.kantei.go.jp/jp/singi/it2/denshigyousei/dai3/siryou2_2.pdf

プログラミングするエンジニアに向けたトレンドメディア

http://postd.cc/

freeのBlu-ray Playerプレーヤー

http://www.leawo.org/blu-ray-player/

オープンソースのネットワークアクセス制御、「PacketFence 5.0」リリース

http://osdn.jp/magazine/15/04/22/070200

cygwin に変わるターミナル環境

http://rcmdnk.github.io/blog/2015/01/24/computer-windows-remote/

日本標準産業分類

http://www.soumu.go.jp/toukei_toukatsu/index/seido/sangyo/

pysonインストール

http://sphinx-users.jp/gettingstarted/install_windows.html

webhook

https://sendgrid.kke.co.jp/blog/?p=1851

Cacoo

http://techacademy.jp/magazine/1704

Cyberduck

https://cyberduck.io/

loadrunner 負荷試験ツール

https://www.microfocus-enterprise.co.jp/adm-blog/2015/07/-hp-loadrunner.html

javascript開発ツール

javascriptの動作確認が出来るサイト
http://jsfiddle.net/

javaでURLを分解するサンプル
https://shanabrian.com/web/javascript/pathinfo.php

ブートローダー

Mac/windows PC で使えるらしい

Clover EFI bootloader

https://ja.osdn.net/projects/sfnet_cloverefiboot/

↑のインストールツール

bootdisk utility
http://cvad-mac.narod.ru/index/bootdiskutility_exe/0-5

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

ページ遷移してもdetailsを開きっぱなしにするJS

素人がchromeでページ遷移をしてしまうとdetailsとsummaryのアレ(以下details)が閉じちゃうのをどうにかしたくて作りました。

動機

小説サイトを改装中、1ページに全ジャンル詰め込みたくてdetails導入してみたんですよ。 Firefoxでは期待通りに動いてくれて、ウッキウキでChrome動作確認したところ、 リンクをクリックして小説読んだあとブラウザバックで戻ってきたらdetails閉じてた。
なんでや!!!!!
ということで仕方なく門外漢がjavascriptに手を出しました。ぐぬぬ。

つくったやつ

See the Pen detailsを開きっぱなしにするやつ by maine (@maimaine) on CodePen.

使い方メモ

主に自分のために忘れそうなので書いておきます。

①htmlでdetailsとsummaryにそれぞれidをつける

details.html
<details id="sample">
  <summary id="sample_s">サンプル1</summary>

サンプルではdetailsに「sample」、summaryに「sample_s」とidをつけています。

②jsを書き換える

details.js
open('openSAMPLEsave', "sample");
details.js
document.getElementById("sample_s").onclick = function() {
 stSave('openSAMPLEsave');
}

「sample」「sample_s」はそれぞれ①でつけたidに書き換えてください。
「openSAMPLEsave」は自由に名付けて大丈夫です。こちらもdetailsごとに別の名前をつけてください。

解説のようなもの

簡単に言うと、セッションストレージを使ってdetailsが開いたかどうかを保存してくれるようにしました。

セッションストレージというのは、ブラウザに情報を保存しておいてくれる便利機能らしいです。

そしてこの子、「ブラウザが」「タブごとに」保存してくれるらしいので、 通信とかすることなく、タブを消しちゃえばその情報は消えてくれるという。 すごい!気軽に使えるいい子だ!!

その他

◆jsファイルってどうやって使うんだっけ

</body>タグの直前に

<script type="text/javascript" src="details.js"></script>

って記入すれば動いてくれます。

◆JS導入できない環境なんだけど

夢小説サイトあるある。JSファイルの中身をコピペして、

<script type="text/javascript">
</script>

このタグの中に入れて</body>タグの直前に置けばOKです。

あとがき

たぶんこういうの見る方々はわたしなんかよりもっとできるひとたちなんだろうけど、もしかしたらわたしと同じようなひとが検索かけてたら悩む手間を省けるかなって思って投稿してみました。もしお役に立てたのなら嬉しいです。
あとなんかもっといい方法あったら教えてください。

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

【JavaScript】オブジェクトから特定のプロパティを抽出

やりたいこと

const obj = { id: 1, name: "taro", age: 20 }
// これを
// { name: "taro", age: 20 }
// このように任意のプロパティだけ抽出したい

配列でいうfilterのようなことをオブジェクトでもやりたくなる。

方法

const obj = { id: 1, name: "taro", age: 20 }
const picked = (({ name, age })=>({ name, age }))(obj)
console.log(picked) // { name: "taro", age: 20 }

解説

const picked = (({ name, age })=>({ name, age }))(obj)

これは

const pick = ({ name, age }) => {
 return { name, age }
}
const picked = pick(obj)

やっていることはこれと同じです。

const pick = ({ name, age }) => ({ name, age })
const picked = pick(obj)

returnを括弧で省略。

const picked = (({ name, age })=>({ name, age }))(obj)

呼び出しまで一行で行う。

おまけ

lodashのpickでも同じことができます。

const picked = _.pick(obj, ["name", "age"])
console.log(picked) // { name: "taro", age: 20 }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vueでフォームを表示せずに画像の更新を行う方法

結論

こんな感じ。色と配置は適当なので参考にしないでください。
Image from Gyazo

See the Pen image submit with vue by msickpaler (@msickpaler) on CodePen.

clearImage() {
  this.image = "https://i.gyazo.com/459b578735367db587a1193f9afe84da.png"
  document.getElementById('input_image').value = '' //忘れやすい
}

inputの値を初期化しないと、
画像選択→クリア→同じ画像選択
したときに2回目の画像選択でinputの値が変わらず@changeで発火しないのでちゃんと初期化しましょう。

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

テキストエリアの文字列を置換して、欲しい形に処理する

背景

本業で広告運用をしながら、勉強でプログラミングをしています。
indeedで運用する際に一部の手作業が面倒だと感じていた為、
アウトプットも兼ねて自動化してみました。

問題点

indeedで広告運用すると、どの求人をアカウントに紐付けるかの設定が必要になります。
何も設定しなければ、公開してる求人全てが対象となります。
ただ、ほとんどの企業から要望があります。
その時に、依頼される求人数が多ければ、手作業が増えます。
(除外の設定のみで済むときもあります。)

デフォルトで用意されてる機能

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

直書きもできる

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

依頼求人が20件なら、まだ手作業でも頑張れるけど、
それ以上になるとポチポチ入れるのが面倒でしかないです。

必要な形を一瞬で作成して、直書きの欄にコピペするのを作成しました。

ソースコード

https://kwateru.github.io/indeed_job_ID_replace/

HTML

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css" type="text/css">
    <title>indeed 求人紐付け</title>
</head>
<body>
        <textarea name="kyujin" class="input" cols="30" rows="10" placeholder="12345&#13;54321&#13;98765"></textarea>
        <button type="submit" value="送信" class="btn">送信&コピー</button>
        <textarea name="seikei" class="seikei" cols="30" rows="10"></textarea>
</body>

    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script src="main.js" type="text/javascript"></script>
</html>

簡単に入力する①テキストエリアと②ボタンと③出力先のテキストエリアを作成しました。
placeholderで例を提示してみたけど、デバイス?によっては改行してくれないことに気付きました。

CSS

style.css
.kyujin, .btn, .seikei{
    display: block;
}

最低限のみです。苦手すぎて・・・。今は、使えれば良いかなってことで。
後々、修正する予定です。

js

main.js
$(function(){

    var input = $('.input');
    var button = $('.btn');
    var result = $('.seikei');

    button.on('click', function(){

        var inputVal = input.val();

        // textareaの内容を改行で分割して配列に格納
        var inputValArray = inputVal.split('\n');

        // 各行に対して処理する
        var display = "";
        for(i = 0; i < inputValArray.length; i++){
            display += '"' + inputValArray[i] + '"' + "\n";
        }

        // 改行を「 OR 」に置換する
        var str = display.replace(/\r?\n/g, ' OR ')
        result.val('refnum:(' + str + ')');

        // copyする
        result.select();
    })

});

不明なところ

末尾に余分なORが出る

1234
4321
↑テキストエリアに入力して、処理すると、
refnum:("43141" OR "41241" OR "41412" OR )

for文の理解

①display += '"' + inputValArray[i] + '"' + "\n";
②display = '"' + inputValArray[i] + '"' + "\n";
2だと1行目しか処理されないこと。

他にも色々あるが、上手く言語化出来ないので、
アウトプットを重ねていくしかない。

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

【写真とコード付き】Vue.jsで書くイベント処理の方法【4選】


はじめに

この記事はVue.jsの基本は抑えられている程で、話を進めていきます。もしVue.jsを初めて触ると言う方は、こちらの記事を参照していただければと思います。

⬇️【写真とコード付き】Vue.jsの構築から基本的な書き方まで1から解説【超初心者向け】
https://qiita.com/yuki4839/items/62f40564e3f4c8dbfc51

では今回は、Vue.jsでイベントの発火によく使用されるであろう、v-on ディレクティブについて、詳しく解説していきます。

早速いきましょう。



実行環境

使用ツール、デバイスはこちら

  • Google chrome
  • Mac OS Catalina
  • Visual Studio Code


また今回使用するディレクトリ階層はこちら。

ディレクトリ階層
─ root(任意のディレクトリ)
│
├─ index.html
│
├─ css
│   └ style.css
│
└─ js
    └ main.js


各ファイルの初期値はこちら。

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

  <link rel="stylesheet" href="./css/style.css">
</head>

<body>

  <div id="web">
    <p>
      {{ context }}
    </p>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
  <script src="./js/main.js"></script>
</body>

</html>
style.css
/* 出力結果を見やすくするためのスタイルです */
body {
  background-color: #add8e6;
}

#web {
  background-color: #fff;
  margin: 20px;
  padding: 20px;
  width: 300px;
}
main.js
const web = new Vue({
  el: '#web',
  data: {
    context: `Hello Vue.js!`
  }
})


現時点での出力結果はこちら。

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



v-on

まず v-on ディレクティブについて簡単に説明すると、ボタンなどがクリックされた時に、何らかのイベントを発生させる事ができるディレクティブになります。

クリック処理

最初は v-on ディレクティブで、最も基本的な処理についてです。文章で伝えるよりも、コードを見て理解してもらうのが早いと思いますので、まずは実際のコードをご覧ください。

index.html
<!-- headタグ省略 -->
<body>

  <div id="web">
    <button v-on:click="click">
      Click!
    </button>
    <p>
      {{ time }}
    </p>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
  <script src="./js/main.js"></script>

</body>
main.js
const web = new Vue({
  el: '#web',
  data: {
    time: ''
  },
  methods: {
    click: function() {
      this.time = new Date().toLocaleString();
    }
    }
})


ポイントは、buttonタグに v-on:click の属性がある事と、js側では属性値に対応した function() が定義されている点です。

動作内容としては、ボタンがクリックされると、click function() が動作し、中の処理が実行されます。(今回なら現在時刻を出力。)

では動作確認。
まずデフォルトの表示がこちら。

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

次にボタンをクリックした結果がこちら。

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



1度のみの処理

続いて v-on ディレクティブで、画面読み込み後に1度だけ発火させたいイベント処理の方法です。まずコードはこちら。

index.html
<!-- headタグ省略 -->
<body>

  <div id="web">
    <button v-on:click.once="click">
      Click!
    </button>
    <p>
    {{ time }}
    </p>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
  <script src="./js/main.js"></script>

</body>
main.js
const web = new Vue({
  el: '#web',
  data: {
    time: ''
  },
  methods: {
    click: function() {
      this.time = new Date().toLocaleTimeString()
    }
  }
})

ポイントは v-on:click の後に .once を記述している点です。これにより、画面読み込み後は1度イベントが発火すると、そのあとは発火しなくなります。

では動作確認。
まずデフォルトの表示がこちら。

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

次にボタンをクリックした結果がこちら。

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

一見通常のクリック処理と同じですが、このあとボタンを押しても、画面の読み込みを行わない限り更新はされません。(先ほどの .once なしの記述の場合は、何度も更新する事ができます。)



引数を利用した処理

続いて v-on ディレクティブを使用した属性値に、引数を渡した際の処理の方法です。まずはコードはこちら。

index.html
<!-- headタグ省略 -->
<body>

  <div id="web">
    <button v-on:click="clickHandler('Hello Vue.js!')">
      Click!
    </button>
    <p>
    {{ context }}
    </p>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
  <script src="./js/main.js"></script>

</body>
main.js
const web = new Vue({
  el: '#web',
  data: {
    context: ''
  },
  methods: {
    clickHandler: function(message) {
      this.context = message
    }
  }
})

ポイントは v-on:click属性 の属性値に、関数名と引数を設定。そしてボタンがクリックされたら、js側で用意されていたイベントが発火と言う仕組みです。

上記の場合は引数で受け取った値を、そのままcontextに代入しているので、出力結果は引数の値になります。


では動作確認です。まずデフォルトの表示がこちら。

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

次にボタンをクリックした結果がこちら。

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



省略記法

最後に v-on省略した書き方についてです。まずコードはこちら。(jsは先程と同様です。)

index.html
<!-- headタグ省略 -->
<body>

  <div id="web">
    <button @click="clickHandler('Hello Vue.js!')">
      Click!
    </button>
    <p>
    {{ context }}
    </p>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
  <script src="./js/main.js"></script>

</body>
main.js
const web = new Vue({
  el: '#web',
  data: {
    context: ''
  },
  methods: {
    clickHandler: function(message) {
      this.context = message
    }
  }
})

ポイントは v-on: の部分が、@ に変わった点です。このように省略して書く事が可能です。もしプロジェクトで使用する際は、どちらかに統一しておくと、可読性が上がると思われます。

念のため動作確認。
まずデフォルトの表示がこちら。

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

次にボタンをクリックした結果がこちら。

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

先程と全く同じ結果が得られました。



まとめ

以上が v-on ディレクティブで良く使われるであろう、イベント処理の記述方法になります。もう一度まとめておくと、以下の通りです。

  • クリック処理
    • v-on:click="click"
  • 1度だけの処理
    • v-on:click.once="click"
  • 引数を利用した処理
    • v-on:click="clickHandler('Hello Vue.js!')
  • 省略記法
    • @click="clickHandler('Hello Vue.js!')"


また補足として、v-on ディレクティブは他にも多数の使用方法があります。多くは「これいつ使う?」って感じだったので、今回は紹介しておりませんが、もし興味がある方は公式リファレンスをご参照ください。

イベントハンドリング(Vue.js公式リファレンス)
https://jp.vuejs.org/v2/guide/events.html

最後まで読んでいただき、ありがとうございました!





筆者:yuki|IT業界のリアルな転職事情など発信|元トップ営業マン(訪販)→未経験からエンジニア転職へ
Qiita:https://qiita.com/yuki4839
Twitter:https://twitter.com/yukifullstack

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

google spread sheet api v4 で clientからjavascriptでシートに行の挿入を行ったメモ

概要

前回まではサーバ側でスプレッドシートにアクセスしていた。
クライアント側からのアクセスも試してみる。

基本は、公式ドキュメントの通りに作ればよい。
APIキーとクライアントIDについてはそれぞれ作る必要があるので注意。

認証情報の準備

プロジェクトの準備はできているものとする。*
グーグルクラウドコンソール( https://console.cloud.google.com/?hl=ja ) からログインして認証情報画面に移動。
image.png
API キーとクライアントIDの2つを作る必要がある。
image.png

APIキーの作成

image.png
仕様予定のAPIの有効化を忘れていないかのチェックがてら、制限しておくとよい。
保存を忘れないこと

クライアントIDの作成

開発用のURLや本番用のURLを登録する。IPアドレス(127.0.0.1)では登録できない。
また、OAuth同意画面でスコープを設定するためにhttpsしか登録できない場合がある。後述。
image.png

APIキーとクライアントIDのコピー

作成したらコピーする。

image.png

ソース

  • ポップアップダイアログが表示されるパターン。chromeにはブロックされるので、サンプル通りボタンを作るほうがよいかも。今回はとりあえず動かしたかったのでこれで。

認証

  <script async defer src="https://apis.google.com/js/api.js"
    onload="this.onload=function(){};handleClientLoad()"
    onreadystatechange="if (this.readyState === 'complete') this.onload()">
  </script>
  <script>
const CLIENT_ID = 'コピーしたクライアントID.apps.googleusercontent.com';
const API_KEY = 'コピーしたAPIキー';
const SCOPES = ['https://www.googleapis.com/auth/spreadsheets'];
const DISCOVERY_DOCS = ["https://sheets.googleapis.com/$discovery/rest?version=v4"];

function handleClientLoad() {
  gapi.load('client:auth2',  authorization);
}

async function authorization() {
  try{
    const auth = await authorize()
    await gapi.client.init({
      apiKey: API_KEY,
      clientId: CLIENT_ID,
      scope: SCOPES,
      discoveryDocs: DISCOVERY_DOCS,
    })
  }catch(err){
    console.log('authError', err)
  }
}
function authorize(){
  return new Promise((resolve, reject) => {
    gapi.auth.authorize({client_id: CLIENT_ID, scope: SCOPES, immediate: false}, function(authResult) {
      if (authResult && !authResult.error) {
        resolve(authResult)
      } else {
        reject(authResult.error)
      }
    });
  })
}
  </script>
</body>

スプレッドシートに行を追加する部分。若干Node.jsとは書き方が異なるので注意。

  const params = {
      spreadsheetId: '1JVa25s339f4kNXhdgHXI4dVq3FGMHrT8x7ZvhXKRDMY',
      range: 'A1',
      valueInputOption: 'USER_ENTERED',
      insertDataOption: 'INSERT_ROWS',
    };
    const body = {
      values: [["test","message"]],
    }

    try {
      const response = (await gapi.client.sheets.spreadsheets.values.append(params, body)).data;
      console.log(JSON.stringify(response, null, 2));
    } catch (err) {
      console.error(err);
    }

httpsについて

  • OAuth同意画面の編集で、機密性の高いスコープを選ぼうとすると、クライアントIDで登録するURLでhttpsしか選べなくなる。 image.png

開発用では不便だなと思ったけれど、少し調べたら、webpackのサーバは簡単にhttps化できるようであった。

https://webpack.js.org/configuration/dev-server/#devserverhttps

webpack.config.js
module.exports = {
  //...
  devServer: {
    https: true
  }
};

また、Angularの場合も、以下のコマンドと設定でhttps化することができた。

https://angular.io/cli/serve

npm run start -- --ssl
angular.json
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "udonarium:build",
            "disableHostCheck": true
          },
          "configurations": {
            "production": {
              "browserTarget": "udonarium:build:production",
+              "ssl": true
            }
          }
        },

参考

Google APIを使ったJavaScriptで簡単アンケートページ(iPadアプリ風)
https://python5.com/q/pjgkcqok
https://angular.io/cli/serve
https://github.com/hibohiboo/udonarium/commit/1b32bec57943d201831f7f113c20626183bd86a3

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

【TODO アプリ】プログラミング初心者が最初に作ったwebアプリ

1.はじめに

この記事のターゲットは「駆け出しエンジニア」もしくは「エンジニアを全くしらない超初心者」向けです。
特に以下のような悩みを抱えている方の悩みを解決できると思います。


  • プログラミング始めたばっかりだけど何を作ればいいかわからない

  • JavaScriptでできることとは

  • そもそもwebアプリってなんだろう

2.著者紹介

冒頭ですごく偉そうなことを言っていますが本記事の著者はプログラミングを初めて1ヶ月の「超初心者」です。
スキルセットとしては以下のような感じです。


  • HTML&CSSがだいたい分かる(progateで一周)

  • JavaScriptは全然わからない(progateで一周)

  • ちょっとしたデータサイエンスに関する知識


簡単に言うとまだまだポンコツです。

3.アプリ概要

【ホーム画面】
スクリーンショット 2020-12-12 20.58.36.png

使い方

  • 入力フォームにタスクを入力
  • タスクを「add」する
  • タスクが終了次第「done」ボタンを押す

4.作成理由

「アウトプット型の学習をしたかったから」です。

人間は基本的に【アウトプット:7・インプット:3】の学習がもっとも効果的だと言われています。そのため、progateなどのインプット中心の学習を手早く済ませて自分でwebアプリを作ってみたいと考えていました。
そこで「HTML、CSS、JAVASCRIPT」のすべてがまんべんなく理解できるTODOリストに挑戦することにしました。

作成期間

・2日間

5.設計


TODOアプリ(フォルダ)
├── index.html
├── style.css
├── app.js

6.使用技術

フロント:HTML、CSS、JavaScript
データベース:PostgreSQL
バージョン管理:Git hub
エディタ:VSCode

【ソースコード】
https://github.com/ShinKitayama/practice.git

7.機能



1.タスクの追加
2.タスクの消去

8.苦労した点

  • 学習したことがない技術の導入
  • 1からの学習
  • 予期せぬエラーへの対処

9.反省点

  • 一つのエラーに対処するのにすごく時間がかかった
  • 【解決策】技術に関する二次情報でなく、一次情報を探す
  • 全て思いつきで機能を実装した
  • 【解決策】コーディングする前から設計を考えておく
  • 見たことないコードに焦った
  • 【解決策】経験するのみ

10.まとめ

今回私は初めて自分でwebアプリをを作成して見て大変だなあと感じた一方ですごく学びのある経験になりました。
最後に、予想される読者の悩みについて再度確認していきます。

  • プログラミング始めたばっかりだけど何を作ればいいかわからない
  • →TODOアプリを作りましょう
  • JavaScriptでできることとは
  • →「TODOアプリ、ブログ、カレンダー、クイズゲーム」画面の中で動作するものです
  • そもそもwebアプリってなんだろう
  • →Webサイトの中でも多機能で、コメントやデータの加工、商品の購入などができる、インタラクティブなもの(今回私が作成した簡単なTODOリストもwebアプリです)

次はブログについて「HTML、CSS、JAVASCRIPT」を使用して作成しようと思います。

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

開発好きの下剋上 ~ヒューマンミスを阻止するためには手段を選んでいられません~

はじめまして!Graat(グロース・アーキテクチャ&チームス)の名も無きフロントエンジニアです。
この記事はグロースエクスパートナーズ アドベントカレンダー13日目の記事です。

この記事の対象読者は、次のような開発をしている人です。
* Webアプリケーションを開発している人
* ExcelでWeb画面のスクリーンショットを取るのが仕事になりかけてる人
* 修正するたびに想定外のレイアウトの崩れが発生して、心に闇を抱え始めてる人

はじめに

プログラミングってとても楽しいですけれど、仕事で長く続けて積み重なっていくコードは怖いですよね。

とくにWebの画面構築は、シンプルな場合思った通りにレイアウトできるけれど、小さく部品化したり、他と組み合わせて要素がネストした構造になってしまったりすると、長くメンテしていくうちに予期しない見た目の崩れが発生することが多々あるので困ります。

フロントエンド開発における愚痴とかあれこれ

そんな現在のWebアプリケーションは、意外と簡単にEnd To Endテストの自動化の導入ができるようになっていて、ビジネスクリティカルな機能やマネタイズとして外せない機能においては自動化・省力化やSemantic Monitoringなどの導入は、私の身の回りで良くみます。

また、ビジネスクリティカルな機能やマネタイズとして外せない機能以外のテストまで細かく書くと、アプリケーションが肥大化するにつれコストが見合わなくなり、楽をするためにテスト自動化してるはずなのに、テストのために苦しむという負の連鎖を生み出す原因になることもしばしばでした。

かといって機能は増えますし、画面の修正は発生するのが現実です。

修正したあとに些細なレイアウト崩れの確認を怠って、QAチームがいるからいいやって確認を押し付けるのは全体的に見て無駄なコストになるし、チーム全体の空気もわるくなっちゃいますよね。

また、そんなことをしてるとQAチームの稼働があがってビジネスクリティカルな部分のチェックが漏れてしまうのは本当避けたいお話です。

そして、QAチームも人間なので、いくら多重チェックしていたとしても前回の画面を細かく覚えてないから、些細なレイアウト崩れの見落としは当然でてきます。

そうなるとチーム間で冷戦がはじまってプロジェクト全体の空気が険悪に・・・

転生しました

1週目の私はそういう経験をしていたようなので、運よく転生した2週目の私はそうならないよう知識チートにより開発チーム内でレイアウト崩れを早期検知して、平和なスロー開発ライフを目指します!

スロー開発ライフの実現には、次のような物を準備する必要がありました。

  • 画面のしょうもないレイアウトの崩れでも、サクッと見つけれる。
  • わかりやすく、よみやすく、シンプルなテストコードをサクッと書ける。
  • 自分の触っている部品が他の画面で影響ないかがサクッとわかる。

つまり冷凍した袋ドーナッツのごとく安心してサクサク開発したいのです!

そのために私たちがたどり着いた解決方法は、Cypressを利用したビジュアルリグレッションテストの導入でした。

ビジュアルリグレッションテストとは?

一言で言うと、前回と現在の画面を比べ、変化があった場合に検知する仕組みです。

Cypressのビジュアルリグレッションテストはコードベースのe2eと違って、振る舞いやページの遷移、セマンティクスやwai-ariaなどのテストは出来ませんが、可視化可能なものをテストする場合は導入が非常に簡単であり運用のコストも安い優れたソリューションです。

どれくらい簡単なのか興味がある方は、以下のリポジトリのREADMEに書かれている手順をやってみてください。
10分程度で終わるので、サクッと体験できると思います。
サンプルコード

このようなビジュアルリグレッションテストは、次のような構成の時に特に力を発揮します。

  • デザインシステムでUIを構築しているシステム
  • コンポーネント指向のシステム
  • CI/CDが準備されている開発 もちろん、上記以外の構成でも利用できますし、メンテナンスが多々あるのであれば、フラットなHTMLページでも十分対費用効果が高いソリューションです。

また、マイクロフロントエンド構成かつデザインシステムを利用している場合はStorybookなどと合わせた導入であれば、テストの時間や内容を効率化することも可能です。

Cypressを利用したビジュアルリグレッションテストの注意点

しかしながら、実際に運用していくと、次のような点を留意する必要が出てきます。

  • MacやWindowsなどの異なるOSを利用した開発者が複数いる場合でも、皆同じ画像が生成されるようにする必要がある
  • どのタイミングでビジュアルリグレッションテストを実行するのか計画する
  • 1つのテストケースでは1回の画像比較に留めておく(共通レイアウトの変更のせいでテストが将棋倒しにならないようにする)
  • Cypressのsnapshotイベントではresizeが発生(画面が一時的に縮小される)ので、resizeに対する対策をする必要がある
  • 画面の修正をしたら、どうやって比較用の画像を更新するのかの手順を考えておく
  • 時間で new Date()など、ローカル自国を画面に表示しているケースは、実行するごとに画像にずれが出るためcy.clock()などの時間を固定にする必要がある
  • アニメーションがある場合はキャプチャタイミングのズレにより差分が発生するため、なるべくアニメーションはOFFにできるソリューションをアプリに組み込んでおく。
  • 比較用の画像を保存する必要があるため、何処に保存するのかと容量の問題をあらかじめ考慮しておく 細かく画像キャプチャを取ると時間がかかるため、差分テストやクラウドビルドなどを利用する

まとめ

ビジュアルリグレッションテストそのものは導入して5年以上たちますが、立ち上げ当初に行っていたseleniumベースで自作していた時代と比べると、Cypressを利用したビジュアルリグレッションテストは圧倒的にローコストで導入できメンテナンスしやすいため、これは十分他のプロジェクトでも横展開する価値がある物だと実感しています。

下手をすると、適切に導入できればユニットテストよりも維持コストが安く、解決できる不安は自動化ソリューションの中でも随一であるため、本当に何の不安もなくサクッと実装・テストをできるようになります。

さらに、維持コストがやすいためテストケースも結構幅広いユースケースを書けたおかげで開発チーム内でのケアレスミスの発見数が増えQAチームへの負荷が下がり、関係性もすごく良いものになりました。

所感

レイアウト崩れの確認は気力をとっても使うので共通部分の修正は正直嫌でしたが、ビジュアルリグレッションテストを利用することで平和な開発が行えるようになりました。

いまは修正によるレイアウト崩れを気にしていた気持ちもどこかに去って、他チームとの交流も楽しいものとなっています。

みなさんも是非、ビジュアルリグレッションテストを導入して平和なスロー開発ライフをおくってください。

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

[JavaScript] すごく初歩のJavaScript (クラス)

JavaScript (クラス)

JavaScript 自分用の覚え書きです。

JavaScriptでのクラスの使用 : コンストラクタ, メソッド, インスタンス

script.js
class Character {
  // コンストラクタ
  constructor(name, level) {
    this.name = name;
    this.level = level;
  }

  // メソッド
  info() {
    console.log(`名前は${this.name}です`);
    console.log(`レベルは${this.level}です`);
  }
}

// インスタンスの生成
const character = new Character("山田", 20);
character.info();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jQueryとjQuery Mobileでcloneしてみた

はじめに

この記事の筆者はひよっこWebエンジニア(2年目)で、jQueryとJavaScriptはほぼ未経験です。
温かい目で見ていただけると幸いですm(_ _)m
コード例はPHPで動的に書いたものを説明しやすくするためにHTMLに書き直し、なおかつ読みやすいように調整したものですので、間違っているかもしれません。

背景

自社サービスにおいて、cloneを使う機会があったので、
また使う事になったときのためにまとめておくことにしました。

使った技術

  • PHP
  • JS
    • jQuery
    • jQuery Mobile
  • CSS
    • jQuery
    • jQuery Mobile

jQuery編

コード例

<table>
    <tr id="like_fruits_1">
        <th>
            <label>好きなフルーツ</label>
            <input type="hidden" id="like_fruits_count" value="1">
        </th>
        <td>
            <select name="fruits[1]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
            <button type="button" id="add_like_fruits_button" class="button button-pill button-primary" style="width: 100px;"><i class="fa fa-plus-square"></i> 追加</button>
            <button type="button" id="delete_like_fruits_button" class="button button-pill button-caution" style="width: 100px;"><i class="fa fa-trash"></i> 削除</button>
        </td>
    </tr>

    <tr id="clone_like_fruits_template" style="display: none">
        <th></th>
        <td>
            <select name="fruits[xxx]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
        </td>
    </tr>
</table>

<script type=text/javascript>
    $(document).ready(function() {
        // 削除ボタンを無効化する
        $('#delete_like_fruits_button').prop('disabled', true);
    });

    // 追加ボタンをクリックしたときの処理
    $(document).on('click', '#add_like_fruits_button', function() {
        // 追加ボタンが押下されたら削除ボタンを有効化する
        $('#delete_like_fruits_button').prop('disabled', false);

        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 追加したあとの要素数を保存
        $('#like_fruits_count').val(creation_times + 1);

        // clone_like_fruits_templateのクローンを作成
        let copy = $('#clone_like_fruits_template').clone(true);

        // xxxを置換
        copy.html(function(i, oldHTML) {
            return oldHTML.replace(/xxx/g, creation_times+1)
        });

        // styleを外す。
        copy.prop('style', false);

        // 属性を付加する
        copy.attr('id', 'like_fruits_'+(creation_times+1));

        // 現要素の一番最後に格納
        copy.insertAfter('#like_fruits_'+creation_times);
    });

    // 削除ボタンをクリックしたときの処理
    $(document).on('click', '#delete_like_fruits_button', function() {
        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 削除した後の個数を保存する
        $('#like_fruits_count').val(creation_times - 1);

        // 項目の一番最後の行を削除する
        $('#like_fruits_'+creation_times).remove();

        // 一番最初の要素は消せないように削除ボタンを無効化する
        if (creation_times <= 2) {
            $('#delete_like_fruits_button').prop('disabled', true);
        }
    });
</script>

で定義して、「追加」を押すと、

<table>
    <tr id="like_fruits_1">
        <th>
            <label>好きなフルーツ</label>
            <input type="hidden" id="like_fruits_count" value="2">
        </th>
        <td>
            <select name="fruits[1]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
            <button type="button" id="add_like_fruits_button" class="button button-pill button-primary" style="width: 100px;"><i class="fa fa-plus-square"></i> 追加</button>
            <button type="button" id="delete_like_fruits_button" class="button button-pill button-caution" style="width: 100px;"><i class="fa fa-trash"></i> 削除</button>
        </td>
    </tr>

    <tr id="like_fruits_2">
        <th></th>
        <td>
            <select name="fruits[2]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
        </td>
    </tr>

    <tr id="clone_like_fruits_template" style="display: none">
        <th></th>
        <td>
            <select name="fruits[xxx]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
        </td>
    </tr>
</table>

<script type=text/javascript>
    $(document).ready(function() {
        // 削除ボタンを無効化する
        $('#delete_like_fruits_button').prop('disabled', true);
    });

    // 追加ボタンをクリックしたときの処理
    $(document).on('click', '#add_like_fruits_button', function() {
        // 追加ボタンが押下されたら削除ボタンを有効化する
        $('#delete_like_fruits_button').prop('disabled', false);

        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 追加したあとの要素数を保存
        $('#like_fruits_count').val(creation_times + 1);

        // clone_like_fruits_templateのクローンを作成
        let copy = $('#clone_like_fruits_template').clone(true);

        // xxxを置換
        copy.html(function(i, oldHTML) {
            return oldHTML.replace(/xxx/g, creation_times+1)
        });

        // styleを外す。
        copy.prop('style', false);

        // 属性を付加する
        copy.attr('id', 'like_fruits_'+(creation_times+1));

        // 一番最後に格納
        copy.insertAfter('#like_fruits_'+creation_times);
    });

    // 削除ボタンをクリックしたときの処理
    $(document).on('click', '#delete_like_fruits_button', function() {
        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 削除した後の個数を保存する
        $('#like_fruits_count').val(creation_times - 1);

        // 一番最後の行を削除する
        $('#like_fruits_'+creation_times).remove();

        // 一番最初は消せないように削除ボタンを無効化する
        if (creation_times <= 2) {
            $('#delete_like_fruits_button').prop('disabled', true);
        }
    });
</script>

みたいな感じに展開されるように作った。
jQueryは初心者でも結構あっさり実装できた。

jQuery Mobile編

コード例

<table>
    <tr id="like_fruits_1">
        <th>
            <label>好きなフルーツ</label>
            <input type="hidden" id="like_fruits_count" value="1">
        </th>
        <td>
            <select name="fruits[1]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
            <button type="button" id="add_like_fruits_button" class="button button-pill button-primary" style="width: 100px;"><i class="fa fa-plus-square"></i> 追加</button>
            <button type="button" id="delete_like_fruits_button" class="button button-pill button-caution" style="width: 100px;"><i class="fa fa-trash"></i> 削除</button>
        </td>
    </tr>

    <tr id="clone_like_fruits_template" style="display: none">
        <th></th>
        <td>
            <my-select name="fruits[xxx]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </my-select>
        </td>
    </tr>
</table>

<script type=text/javascript>
    $(document).ready(function() {
        // 削除ボタンを無効化する
        $('#delete_like_fruits_button').attr('class', 'button button-pill button-caution button-large ui-btn-inline ui-shadow ui-link disabled');
    });

    // 追加ボタンをクリックしたときの処理
    $(document).on('click', '#add_like_fruits_button', function() {
        // 追加ボタンが押下されたら削除ボタンを有効化する
        $('#delete_like_fruits_button').attr('class', 'button button-pill button-caution button-large ui-btn-inline ui-shadow ui-link');

        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 追加したあとの要素数を保存
        $('#like_fruits_count').val(creation_times + 1);

        // clone_like_fruits_templateのクローンを作成
        let copy = $('#clone_like_fruits_template').clone(true);

        // xxxを置換。my-selectを置換
        copy.html(function(i, oldHTML) {
            return oldHTML.replace(/xxx/g, creation_times+1).replace(/my-select/g, 'select')
        });

        // styleを外す。
        copy.prop('style', false);

        // 属性を付加する
        copy.attr('id', 'like_fruits_'+(creation_times+1));

        // 一番最後に格納
        copy.insertAfter('#like_fruits_'+creation_times).trigger('create');
    });

    // 削除ボタンをクリックしたときの処理
    $(document).on('click', '#delete_like_fruits_button', function() {
        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 一番最初の要素しか無いときは何も処理をさせない
        if (creation_times === 1) {
            return;
        }

        // 削除した後の個数を保存する
        $('#like_fruits_count').val(creation_times - 1);

        // 一番最後の行を削除する
        $('#like_fruits_'+creation_times).remove();

        // 一番最初は消せないように削除ボタンを無効化する
        if (creation_times <= 2) {
            $('#delete_like_fruits_button').attr('class', 'button button-pill button-caution button-large ui-btn-inline ui-shadow ui-link disabled');
            return;
        }
    });
</script>

で定義して、「追加」を押すと、

<table>
    <tr id="like_fruits_1">
        <th>
            <label>好きなフルーツ</label>
            <input type="hidden" id="like_fruits_count" value="2">
        </th>
        <td>
            <select name="fruits[1]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
            <button type="button" id="add_like_fruits_button" class="button button-pill button-primary" style="width: 100px;"><i class="fa fa-plus-square"></i> 追加</button>
            <button type="button" id="delete_like_fruits_button" class="button button-pill button-caution" style="width: 100px;"><i class="fa fa-trash"></i> 削除</button>
        </td>
    </tr>

    <tr id="like_fruits_2">
        <th></th>
        <td>
            <select name="fruits[2]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
        </td>
    </tr>

    <tr id="clone_like_fruits_template" style="display: none">
        <th></th>
        <td>
            <my-select name="fruits[xxx]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </my-select>
        </td>
    </tr>
</table>

<script type=text/javascript>
    $(document).ready(function() {
        // 削除ボタンを無効化する
        $('#delete_like_fruits_button').attr('class', 'button button-pill button-caution button-large ui-btn-inline ui-shadow ui-link disabled');
    });

    // 追加ボタンをクリックしたときの処理
    $(document).on('click', '#add_like_fruits_button', function() {
        // 追加ボタンが押下されたら削除ボタンを有効化する
        $('#delete_like_fruits_button').attr('class', 'button button-pill button-caution button-large ui-btn-inline ui-shadow ui-link');

        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 追加したあとの要素数を保存
        $('#like_fruits_count').val(creation_times + 1);

        // clone_like_fruits_templateのクローンを作成
        let copy = $('#clone_like_fruits_template').clone(true);

        // xxxを置換。my-selectを置換
        copy.html(function(i, oldHTML) {
            return oldHTML.replace(/xxx/g, creation_times+1).replace(/my-select/g, 'select')
        });

        // styleを外す。
        copy.prop('style', false);

        // 属性を付加する
        copy.attr('id', 'like_fruits_'+(creation_times+1));

        // 一番最後に格納
        copy.insertAfter('#like_fruits_'+creation_times).trigger('create');
    });

    // 削除ボタンをクリックしたときの処理
    $(document).on('click', '#delete_like_fruits_button', function() {
        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 一番最初の要素しか無いときは何も処理をさせない
        if (creation_times === 1) {
            return;
        }

        // 削除した後の個数を保存する
        $('#like_fruits_count').val(creation_times - 1);

        // 一番最後の行を削除する
        $('#like_fruits_'+creation_times).remove();

        // 一番最初は消せないように削除ボタンを無効化する
        if (creation_times <= 2) {
            $('#delete_like_fruits_button').attr('class', 'button button-pill button-caution button-large ui-btn-inline ui-shadow ui-link disabled');
            return;
        }
    });
</script>

これでほぼjQueryと同じ感じに展開された。

名前は似ているが、実は結構違った「jQuery」と「jQuery Mobile」

「jQuery Mobile」は「Mobile」とつくだけだから、「jQuery」で書いたコードがそのまま動くでしょ。なんて軽く思っていたら違う部分が結構あった。
jQuery Mobileでは、cloneで追加されたセレクトボックスを選択しても、画面上に反映されなかった。
これの解消に2日くらい時間を要した。

理由は、DOMとしてレンダリングされたとき、すでに「jQuery Mobile」によってselectui-selectに変換されていたからだった。
今回は対処法としてDOMの段階ではmy-selectみたいな「jQuery Mobile」に勝手に変換されないようにして解決させた。

さすがに「Java」と「JavaScript」のように全く違う言語とまではいかないけど、それでも結構違った。

最後に、今回作成した成果物をご紹介

jQuery

追加ボタンを押す前
スクリーンショット 2020-12-12 20.38.32.png

追加ボタンを押した後
スクリーンショット 2020-12-12 20.38.49.png

jQuery Mobile

追加ボタンを押す前
スクリーンショット 2020-12-12 20.39.26.png

追加ボタンを押した後
スクリーンショット 2020-12-12 20.39.40.png

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

JavaScriptでHTTPSなしで暗号化したデータを送りたい。

目的

image.png

使用するライブラリ

https://github.com/kjur/jsrsasign

jsrsasignはJavaScriptのライブラリでブラウザサイド、node.js側のサーバーサイドで以下のような暗号化を行えます。

RSA/RSAPSS/ECDSA/DSA signing/validation, ASN.1, PKCS#1/5/8 private/public key, X.509 certificate, CRL, OCSP, CMS SignedData, TimeStamp, CAdES JSON Web Signature/Token/Key

事前準備

opensslで公開鍵と秘密鍵を作成します。

openssl genrsa 2024 > secret.key 
openssl rsa -pubout < secret.key > public.key

サンプル

以下の例ではブラウザ側で公開鍵で暗号化したデータをnode.js側で秘密鍵で復号化しています。

server.js
const express = require("express");
const app = express();
const fs = require('fs');
const bodyParser = require('body-parser')

const port = 3000; // 1024以下にした場合は管理者権限が必要になります.

const http = require("http");
const server = http.createServer(app);

const rs = require('jsrsasign');
const rsu = require('jsrsasign-util')
/**
openssl genrsa 2024 > secret.key 
openssl rsa -pubout < secret.key > public.key
 */
const secretKey = rs.KEYUTIL.getKey(rsu.readFile('secret.key'))

app.use(express.static(__dirname + "/public"));
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())

app.post('/login', async (req, res) => {
  console.log(req.body.data)
  const data = JSON.parse(rs.KJUR.crypto.Cipher.decrypt(req.body.data,secretKey,'RSA'))
  console.log(data.time, (new Date).getTime(), (new Date).getTime()-data.time)
  if ((new Date).getTime()-data.time > 1000 * 60) {
    res.status(401).send('時間切れ')
    return
  }
  const result = {
    message: '応答:' + data.message,
  }

  res.status(200).send(result)
})

server.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});
public/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>シンプルチャット</title>
    <meta charset="UTF-8" />
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/8.0.20/jsrsasign-all-min.js"></script>
  </head>
  <body>
    <div id="app">
      <div>
        <input v-model="sendName"></input>
        <button v-on:click="send">Send</button>
      </div>
    </div>
<script>
  console.log('app')
const app = new Vue({
  el: '#app',
  data: {
    sendName : "abcde"
  },
  computed: {
  },
  created : function() {
  },
  methods: {
    send : function() {
      console.log('send')
      // 以下のコマンドで作った公開キー
      // openssl genrsa 2024 > secret.key 
      // openssl rsa -pubout < secret.key > public.key
      const publicKeyStr = `
-----BEGIN PUBLIC KEY-----
MIIBHjANBgkqhkiG9w0BAQEFAAOCAQsAMIIBBgKB/gC1UZYJkhTJpfitHn0Jv6Ms
b15tHhsYO1DHICrRwNMkePCm1hWUbK3aG+Q173SrO1yR1qadPsz3heMbDwwqaU3t
0CMsdaLzPdCLiTT7HFXHkc1TI/ltwg0NAo4YrHN89WFk7/zGquy8ekeZFX21b2Xf
sqtiQCkHf6W2XIOgSo5AbH8V6wPgzCPBn1hu2lL5btF10Rbt9KkW/3WiRt/U06wD
5QgwJZ4A140dzea3mBSH6r0bje9h3nHmzqpwA5a9QxSL1HYH4E9VEV8FDwIAI3Qw
O2kxvpTKG0qst3i6nxcvRHmeWFakPnCqnyuqV31FJVf8cebbMlVePUb2IfCbAgMB
AAE=
-----END PUBLIC KEY-----
`
      console.log(this.sendName)
      const data = {
        message: this.sendName,
        time: new Date().getTime()
      }
      const publicKey = KEYUTIL.getKey(publicKeyStr)
      const encyptData = KJUR.crypto.Cipher.encrypt(JSON.stringify(data), publicKey, 'RSA')

      axios.post('/login', {data: encyptData})
      .then(function (response) {
        alert(JSON.stringify(response.data))
      })
      .catch(function (error) {
        alert(error)
      })
    },

  }
})

</script>
  </body>
</html>

まとめ

jsrsasignを利用することでブラウザ側でも暗号化できます。
でも、素直にHTTPS化した方がいいと思うので使うことはないでしょう。

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

AWS SDK for JavaScript をdeveloper toolのコンソールから使用する

主題

Amazon S3の署名付きURLをChromeの開発者ツールだけで発行した話。環境構築不要(認証情報さえあれば)。
ブラウザでSDKが使えるのを知り、手軽に試せないかと思い。

ブラウザスクリプトの使用開始
https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v2/developer-guide/getting-started-browser.html

手順

認証情報を拝借するユーザーに必要なポリシーは指定するメソッドだけ(以下の場合はs3:GetObject権限のみ)。

ブラウザを開き、URLにabout:blankを入力して画面をまっさらにする(これしないと動かない、なんで)。
以下のどちらかで、SDK読み込み。

(script = document.createElement('script')).src = 'https://sdk.amazonaws.com/js/aws-sdk-2.809.0.min.js'
document.getElementsByTagName('head')[0].appendChild(script)

または

var ele = document.createElement("script");
ele.type = "text/javascript";
ele.src = "https://sdk.amazonaws.com/js/aws-sdk-2.809.0.min.js";
document.body.appendChild(ele);

以下で関数定義。アクセスキー、シークレットアクセスキー、バケット名を適宜変更。

    const S3 = new AWS.S3({
        accessKeyId: 'xxxxxxxxx',
        secretAccessKey: 'yyyyyyyyy',
        signatureVersion: 'v4',
        region: 'ap-northeast-1'
    });

    function getSignedUrl(fileName) {
        const params = {
            Bucket: "YOUR_BUCKET_NAME",
            Key: fileName,
            Expires: 60
        };
            S3.getSignedUrl("getObject", params,function(err,url){
                console.log(url)
            });
        return;
    }

以降は getSignedUrl("hello.html");を実行すると、YOUR_BUCKET_NAME/hello.htmlに対して60秒GETできるURLが出力される。

参考

コンソールでのライブラリの読み込み

Chromeコンソールでお手軽クローリング
https://qiita.com/cognitom/items/babefa7b85ffc050d4bd

ChromeのJavascriptコンソールで外部ライブラリを使いたいとき
http://duyoji.hatenablog.com/entry/2013/05/01/172731

S3の関数記述部分

getSignedUrl(operation, params, callback) ⇒ String
https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property

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

ドルガバで学ぶ turf.js の論理演算機能

年に一度くらい魔が差す時があるので。
元ネタ と異なり、文字の輪郭を忠実にポリゴンとして扱います。つまり「D」は穴あきポリゴンです。

image.png

const polygonD = {
  "type":"Feature",
  "properties":{},
  "geometry":{
    "type":"Polygon",
    "coordinates":[ 省略 ]
  }
};

const polygonG = {
  "type":"Feature",
  "properties":{ },
  "geometry":{
    "type":"Polygon",
    "coordinates":[ 省略 ]
  }
};

const polygonFrame = {
  "type": "Feature",
  "properties": {},
  "geometry": {
    "type": "Polygon",
    "coordinates": [ 省略 ]
  }
};

const map = new mapboxgl.Map({
  container: 'map',
  style: {
    version: 8,
    sources: {
      OSM: {
        type: "raster",
        tiles: [ "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png" ],
        tileSize: 256,
        attribution:
        "OpenStreetMap",
      },
      sources: {
        'type': 'geojson',
        'data': {
          "type": "FeatureCollection",
          "features": []    
        }
      },
      results: {
        'type': 'geojson',
        'data': {
          "type": "FeatureCollection",
          "features": []    
        }
      },
    },
    layers: [
      {
        id: "BASEMAP",
        type: "raster",
        source: "OSM",
        minzoom: 0,
        maxzoom: 18,
      },
      {
        id: 'sources',
        type: 'line',
        source: 'sources',
        paint: {
          'line-color': '#088',
          'line-width': 5,
          'line-opacity': 0.8,
        }
      },
      {
        id: 'results',
        type: 'fill',
        source: 'results',
        paint: {
          'fill-color': 'red',
          'fill-opacity': 1,
        }
      }      
    ],
  },      
});

map.once('load', () => {

  const sourceFeatures = [];

  // 元図形
  sourceFeatures.push({
    d: polygonD,
    g: polygonG,
    frame: polygonFrame,
  });

  // 上左
  sourceFeatures.push({
    d: turf.transformTranslate(polygonD, 70, 90, { units: 'kilometers' }),
    g: turf.transformTranslate(polygonG, 70, 90, { units: 'kilometers' }),
    frame: turf.transformTranslate(polygonFrame, 70, 90, { units: 'kilometers' }),
  });

  // 上中
  sourceFeatures.push({
    d: turf.transformTranslate(polygonD, 140, 90, { units: 'kilometers' }),
    g: turf.transformTranslate(polygonG, 140, 90, { units: 'kilometers' }),
    frame: turf.transformTranslate(polygonFrame, 140, 90, { units: 'kilometers' }),
  });

  // 上右
  sourceFeatures.push({
    d: turf.transformTranslate(polygonD, 210, 90, { units: 'kilometers' }),
    g: turf.transformTranslate(polygonG, 210, 90, { units: 'kilometers' }),
    frame: turf.transformTranslate(polygonFrame, 210, 90, { units: 'kilometers' }),
  });

  // 下左
  sourceFeatures.push({
    d: turf.transformTranslate(sourceFeatures[1].d, 60, 180, { units: 'kilometers' }),
    g: turf.transformTranslate(sourceFeatures[1].g, 60, 180, { units: 'kilometers' }),
    frame: turf.transformTranslate(sourceFeatures[1].frame, 60, 180, { units: 'kilometers' }),
  });

  // 下中
  sourceFeatures.push({
    d: turf.transformTranslate(sourceFeatures[2].d, 60, 180, { units: 'kilometers' }),
    g: turf.transformTranslate(sourceFeatures[2].g, 60, 180, { units: 'kilometers' }),
    frame: turf.transformTranslate(sourceFeatures[2].frame, 60, 180, { units: 'kilometers' }),
  });

  // 下右
  sourceFeatures.push({
    d: turf.transformTranslate(sourceFeatures[3].d, 60, 180, { units: 'kilometers' }),
    g: turf.transformTranslate(sourceFeatures[3].g, 60, 180, { units: 'kilometers' }),
    frame: turf.transformTranslate(sourceFeatures[3].frame, 60, 180, { units: 'kilometers' }),
  });

  sourceFeatureCollection = turf.featureCollection(sourceFeatures.reduce((acc, x) => {
    acc.push(x.d);
    acc.push(x.g);
    acc.push(x.frame);
    return acc;
  }, []));
  map.getSource('sources').setData(sourceFeatureCollection);


  // D AND G -> intersect
  const DandG = turf.intersect(sourceFeatures[1].d, sourceFeatures[1].g);

  // D OR G -> union
  const DorG = turf.union(sourceFeatures[2].d, sourceFeatures[2].g);

  // D XOR G -> (D - G) + (G - D)
  const DminusG = turf.difference(sourceFeatures[3].d, sourceFeatures[3].g);
  const GminusD = turf.difference(sourceFeatures[3].g, sourceFeatures[3].d);
  const DxorG = turf.union(DminusG, GminusD);

  // D NAND G -> frame - DandG
  const DnandG = turf.difference(sourceFeatures[4].frame, turf.intersect(sourceFeatures[4].d, sourceFeatures[4].g));

  // D NOR G -> frame - DorG
  const DnorG = turf.difference(sourceFeatures[5].frame, turf.union(sourceFeatures[5].d, sourceFeatures[5].g));

  // D NXOR G -> frame - DxorG
  const tmpDminusG = turf.difference(sourceFeatures[6].d, sourceFeatures[6].g);
  const tmpGminusD = turf.difference(sourceFeatures[6].g, sourceFeatures[6].d);
  const DnxorG = turf.difference(sourceFeatures[6].frame, turf.union(tmpDminusG, tmpGminusD));

  map.getSource('results').setData(turf.featureCollection([
    DandG, 
    DorG,
    DxorG,
    DnandG,
    DnorG,
    DnxorG,
  ]));

  map.fitBounds(turf.bbox(sourceFeatureCollection), { padding: { bottom: 200, top: 200, right: 200, left: 200 }});
});

また例によってだらだら長いコードになってしまったけど、論理演算を行っているのは、最下部のあたり。
turf.js の論理演算機能は、intersect, union, ''difference'' のみです。

intersect は、2つの図形が共に重なり合う領域、つまり AND です。
union は、2つの図形のいずれかの領域、つまり OR。
difference は差分、つまり第一引数の図形から、第二引数の図形の領域を引いた領域です。

この3つだけでも、組み合わせるとそれなりの事はできますねーという例でした。

完全なコード

参考

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

kintoneの「アプリ内で別アプリのレコードを検索、参照したい」というニッチな要望を解決する

はじめに

今回アドベントカレンダー何書こうかな...と悩んでいたのですが、折角なのでチームでの取り組みの一部を軽く紹介していこうと思います。

私達のチームでは、マルチサイズプラットフォーム事業(MSP)におけるデジタルトランスフォーメーション(DX)の取り組みを行っています。
要するにアパレル生産の業務の自動化とか効率化ですね。

今回は事業で使っているkintoneという業務改善プラットフォームで使えるニッチなコードについて書きたいと思います!

元々、有料プラグインとかで実装されている機能みたいですが、なんとかコードで再現できないかと要望を貰い実装してみました。

軽く背景

今回の実装は、とある契約書のドキュメントを自動生成するために作ったアプリのフロントエンド部分に使っています。

ドキュメントはレコード毎に作られており、ドキュメントを発行したいレコードを検索条件をもとに検索し、該当したレコードから発行するレコードのみを選びバックエンド側に情報を送るといった流れです。

正直、既存のプラグインで代用できそうな機能ではありますが、レコードの内容を参照して〜処理したいといった要望も出てきそうだったのでコードで実装することにしました。

特定の条件に一致するレコードを他アプリから検索する

以下の様にレコードの特定フィールドを指定し、検索ボタンを押下します。
form.png

検索ボタン押下後に、検索結果を以下のテーブルに表示します。

スクリーンショット 2020-12-05 16.42.42.png

今回作ったアプリでは発行フィールドを選択することで、検索して列挙されたレコードから必要なレコードだけを絞り込みすることができる仕様になっています。

特定のワードを含んだレコードの要素を検索する

レコードのフィールドコートの頭にsearch_とついているモノだけを抽出します。
search_のキーワードを外しているのは、後々テーブルのフィールドコートの参照に使うためです。

// アプリ内のレコード要素を取得
let record = kintone.app.record.get();

search_key_list = []
for (r_key in record.record){
  if (r_key.indexOf('search_') != -1){
    search_key_list.push(r_key.replace('search_', ''))
  }
}

他のkintoneアプリのレコードを参照する為のクエリを作成する

指定した条件でkintoneのレコードを検索するために、クエリを作成します。

let query_text = ''

for (const [search_key_index, search_key_value] of search_key_list.entries()){
  let search_val = record.record['search_' + search_key_value].value
  if (search_key_index == 0) {
    query_text += search_key_value + ' in ("' + search_val + '")'
  }else{
    query_text += " and " + search_key_value + ' in ("' + search_val + '")'
  }
}

他アプリからレコードを参照する

上記で作成したクエリでkintoneの他アプリからレコードを参照し、検索結果をテーブルに追加した後にレコードを更新しています。
IDと発行のフィールドコートは固定なので別で処理しています。
検索条件やテーブルの項目を編集することで、コードを変えずに検索条件や検索後の結果を変えることができます。

// bodyを作成
let body = {
  "app": "appのID",
  totalCount: true
}
body["query"] = query_text

kintone.api(kintone.api.url('/k/v1/records', true), 'GET', body).then(function(resp){
  return resp;
}, function(error) {
  console.log(error);
}).then(function(resp){
  // 取得したレコードの処理
  let records = JSON.stringify(resp);
  let results = JSON.parse(records);

  for(const [r_index, r_record] of results.records.entries()){

    // dictを新規作成
    let new_table_value = {
      id: r_index,
      value: {
        "発行": {
          type: "CHECK_BOX",
          value: []
        },
        "レコードID": {
          type: "SINGLE_LINE_TEXT",
          value: r_record["$id"].value,
          disabled: true
        },
      }
    }

    // tableのkey分ループを回す
    for(const key of key_list){
      if(key == "発行" || key == "レコードID"){
        // 事前に代入したフィールドは飛ばす
      }else{
        if(r_record[key] == undefined){
          record.record['ログ表示'].value = 'エラーが発生しました。存在しないキーをテーブルで指定しています。'
          kintone.app.record.set(record);
          return
        }
        new_table_value.value[key] = {
          type: "SINGLE_LINE_TEXT",
          value: r_record[key].value,
          disabled: true
        }
      }
    }
    table_set_records.push(new_table_value)
  }
  record.record['テーブル'].value = table_set_records
  record.record['ログ表示用に用意したフィールド'].value = '検索結果は' + results.records.length + '件です。'
  kintone.app.record.set(record);
});

さいごに

kintoneは公式ドキュメントが沢山挙がってたりするので実装しやすかったです。
ただ、アプリにアップするコードの管理はどんな感じでやろうかな...という悩みはありますね。

今回思いつきで実装したところがあるので、もっといい方法あるよー!という方いらっしゃいましたら教えてください〜
(そしてコードがあまり整理できていなくてすみません...)

他にも便利そうな実装があればブログにしていきたいなと思います〜。

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

[Salesforce]外部システムからからファイルを取得/ダウンロード

この記事は「Salesforce Platform Advent Calendar 2020」の第16日目の記事です。

また、この記事はSalesforce 開発者向けブログ投稿キャンペーンへのエントリー記事です。

Salesforce外部からのファイルダウンロードに関して

 今回は、Salesforce外部のシステムからのAPI連携で画像ファイルなどを取得する処理を実装する必要があり、実装に少し困ったのでその方法を書いておきます。
 結論、カスタムで画面を開発して、JavaScriptを使用すればローカルPCへファイルのダウンロードが可能です。今回はVisualforceで実装してみました。また、今回は実際にAPIコールは行わず、静的リソースからバイナリーデータを取得して、疑似的にHttpResponseの値を取得した形にしております。

実際のコード

FileDownload.vfp
<apex:page controller="FileDownloadCtrl">
  <apex:form>
    <apex:pageMessages id="msg" />

    <apex:outputPanel layout="block" style="margin-top: 1rem; margin-left: 1rem;">
      <apex:inputText value="{!fileName}" />
      <apex:commandButton value="ファイルダウンロード" action="{!doDownload}" reRender="msg" oncomplete="doDownload('{!fileName}', '{!fileData}', '{!mimeType}');"
      />
    </apex:outputPanel>
  </apex:form>

  <script>
    function doDownload(fileName, fileData, mimeType) {

      // データがないなら処理を実行しない
      if (!fileData) {
        return;
      }

      // Blobデータの作成
      var bin = atob(fileData);
      var buffer = new Uint8Array(bin.length);
      for (var i = 0; i < bin.length; i++) {
        buffer[i] = bin.charCodeAt(i);
      }
      var blob = new Blob([buffer.buffer], { type: mimeType });

      // ダウンロードを実行
      var link = document.createElement("a");
      link.download = fileName;
      link.href = window.URL.createObjectURL(blob);
      link.click();
      URL.revokeObjectURL(blob);
    }
  </script>
</apex:page>
FileDownloadCtrl.cls
public with sharing class FileDownloadCtrl {
    // コンストラクタ
    public FileDownloadCtrl() {
    }

    // file名
    public String fileName {get; set;}

    // fileデータ
    Transient public String fileData {get; set;}

    // MIMETYPE
    public String mimeType {get; set;}

    // ファイルダウンロード実行
    public void doDownload() {
        List<StaticResource> srList = [SELECT Id, Body, ContentType FROM StaticResource WHERE Name = :fileName];

        if (srList.isEmpty()) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, '該当ファイルがありません'));
        } else {
            this.fileData = EncodingUtil.base64Encode(srList[0].Body);                  // ファイルのデータ
            this.mimeType = srList[0].ContentType;                                      // MIMRタイプ
            this.fileName += FILE_EXTENTION_BY_CONTENT_TYPE.get(srList[0].ContentType); // ファイル名に拡張子をつける
        }

    }

    public FINAL Map<String, String> FILE_EXTENTION_BY_CONTENT_TYPE = new MAP<STring, String>{
        'image/png' => '.png'
        // .jpg, .pdf, .xls, .docなど
    };
}

コードの解説

1.
「ファイルダウンロード」時にダウンロードしたいファイル名を取得し、一致する静的リソースのファイルを取得してきます。
(HttpReqest req = new Httpreqest();としてヘッダーやエンドポイントの設定をします。)

2.
取得してきた静的リソースのBodyをbase64エンコードして、Stringとして変数に保持します。
また、MIMEタイプもStringとして変数に保持します。
(実際には HttpResponseのgetBodyasBlob()メソッドを使用して、Blob型の変数に保持します。)

3.
上記の処理完了後にoncompleteでJavaScriptのメソッドを実行します。
ここで、base64文字列とMIMEタイプを使用して、Blobオブジェクトを作成します。
そのBlobオブジェクトを利用して、createObjectURLメソッドとlinkのダウンロード属性を利用して、クリック時にファイルをダウンロードするようにし、JavaScriptでクリック処理を実行させます。

このあたりの処理の詳細は、下記のサイトを参考にしてください。
Blob-Web API
Base64 のエンコードとデコード
a: アンカー要素

今回、はまって勉強したところ

①MIMEタイプの判定
 今回、使用するAPIではどのようなファイルでもContentTypeがapplication/octet-streamでレスポンスが返ってくるため、ファイルのMIMEタイプを判定することができないというところで少し悩みました。
 結論としては、画像ファイルなどはバイナリデータのヘッダー署名から、そのファイルがjpgなのか、pdfなのか、pdfなのか判断できることが分かりました。その一方で、office関連のxlsやdocファイルなどは、バイナリデータのヘッダー署名からファイルの実態を判断することは難しいということが分かりました。

 下記のサイトで様々なファイルのヘッダー署名を確認することが出来ますが、xlsファイルとdocファイルや、xlsxファイルとdocxファイルは署名が一致しており、その署名からファイルの拡張子を判断することが出来ませんでした。
File Signature Database

今回は、レスポンスのヘッダーにファイル登録時のファイル名が設定されて返ってくる仕様だったため、officeファイルはそのファイル名の拡張子からMIMEタイプを判断することとしました。
 
②IEでのファイルダウンロード
 IEではHTMLのa要素のdownload属性がサポートされていないため、上記のコードではダウンロードが出来ませんでした。こちらに関しては、window.navigater.msSaveBlob()メソッドを使用して、ダウンロードすることが出来ました。

msSaveBlob

終わりに

 今回の実装が必要になるケースはそこまで多くはないと思いますが、この実装を行ったおかげでHttp通信やバイナリーデータに関する知識がかなり深まりました。この知識を活かして、今後新たなSalesforceハックに挑戦してみたいと思いました。

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

Blazor から JavaScript ライブラリを利用する

Blazor における C# と JavaScript との相互運用 (JavaScript Interop) 、また JavaScript ライブラリの利用について解説します。

環境:

  • .NET 5
  • Blazor WebAssembly

JavaScript Interop の挙動を確認する

Blazor で JavaScript Interop を利用する際は、以下のステップが必要になります。

  1. /wwwroot 配下に JavaScript モジュールを配置する
  2. C# から JavaScript モジュールをインポートする
  3. モジュールが公開している関数を C# から呼び出す

まず、呼び出される JavaScript モジュールを以下の通り定義します。

wwwroot/js/interop-sample.js
export function outputLog(obj) {
   console.log(typeof obj, obj);
}

C# のソースコードから上記モジュールをインポートし、関数を呼び出します。まず、次の名前空間への参照を追加します。

InteropSample.razor
@inject IJSRuntime JSRuntime

これにより、以下の手順で JavaScript モジュールを参照できます。

InteropSample.cs
var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
   "import", "./js/interop-sample.js"
);

await module.InvokeVoidAsync("outputLog", "my first interop.");

InvokeVoidAsync() では、実行する関数名を第一引数で、関数へ渡す引数項目をそれ以降の引数で指定します。

これにより引数 obj へ渡した値の型 string とその内容 "my first interop" が、ブラウザーのコンソール上へ出力されます。

string my first interop.

引数として渡せる値は JavaScript 向けに変換が行われます。変換結果の例としては以下の通りです。

InteropSample.cs
await module.InvokeVoidAsync("outputLog", null);
await module.InvokeVoidAsync("outputLog", true);
await module.InvokeVoidAsync("outputLog", false);
// null は undefined へ、bool は boolean へそれぞれ変換される。
//   undefined undefined
//   boolean true
//   boolean false

await module.InvokeVoidAsync("outputLog", 123);
await module.InvokeVoidAsync("outputLog", "456");
await module.InvokeVoidAsync("outputLog", "foo");
// 数値を渡した場合は number へ、文字列を渡した場合は string へ変換される。
//   number 123
//   string 456
//   string foo

// enum MyEnum { Value1 = 7, Value2 = 5, Value3 = 6 }
await module.InvokeVoidAsync("outputLog", MyEnum.Value1);
await module.InvokeVoidAsync("outputLog", MyEnum.Value2);
await module.InvokeVoidAsync("outputLog", MyEnum.Value3);
// 列挙型のメンバーは、対応する整数値へ変換される。
//   number 7
//   number 5
//   number 6

var sampleList = new List<string>() { "aaa", "bbb", "ccc" };
await module.InvokeVoidAsync("outputLog", sampleList);
await module.InvokeVoidAsync("outputLog", sampleList as IEnumerable<string>);
// IEnumerable インターフェイスを備えた型であれば配列へ変換される。
//   object (3) ["aaa", "bbb", "ccc"]
//   object (3) ["aaa", "bbb", "ccc"]

// class SampleClass
// {
//    private string _X = "private field";
//    public string X = "public field";
//    private string _Y { get; set; } = "private property";
//    public string Y { get; set; } = "public property";
// }
var sampleInstance = new SampleClass();
await module.InvokeVoidAsync("outputLog", sampleInstance);
// クラスインスタンスは public プロパティのみを含んだ object へ変換される。
//   object {y: "public property"}

await module.InvokeVoidAsync("outputLog", new Dictionary<int, string>() {
    { 1, "value1" },
    { 2, "value2" },
    { 3, "value3" },
});
// Dictionary は object へ変換される。
//   object {1: "value1", 2: "value2", 3: "value3"}

await module.InvokeVoidAsync("outputLog", new
{
   Item1 = "foo",
   Item2 = "hoo",
   Item3 = 123
});
// 匿名型も object へ変換される。
//   object {item1: "foo", item2: "hoo", item3: 123}

var sampleTuple = (909, 703);
await module.InvokeVoidAsync("outputLog", sampleTuple);
await module.InvokeVoidAsync("outputLog", sampleTuple.ToString());
// Tuple は変換を行えない。
//   object {}
//   string (909, 703)

var sampleValueTuple = (x: 123, y: "sample");
await module.InvokeVoidAsync("outputLog", sampleValueTuple);
await module.InvokeVoidAsync("outputLog", sampleValueTuple.ToString());
// ValueTuple も同様。
//   object {}
//   string (123, sample)

この変換処理は以下の注意点があります。

  • クラス インスタンスの持つプロパティ名は、先頭を小文字に変換される
  • TupleValueTuple といった型を持つ値は変換を行えない

Tuple が利用できないのは意外な気もしますが、JavaScript 側で対応するデータ構造がないことが理由なのかもしれません。

JavaScript ライブラリを利用した処理をモジュールとして定義する

JavaScript Interop による関数の呼び出しと、関数へ渡した値が変換される際の挙動について確認しました。続いて、JavaScript ライブラリを用いて DOM を操作するパターンについて確認します。

例として、Chart.js を利用して画面上へグラフを出力するまでの流れを確認します。

Blazor のテンプレート プロジェクトでは、連日の気温を一覧表示する FetchData.razor というサンプル コンポーネントが含まれています(気温の数値自体はダミーデータとなっています)。これをもとに、気温の変化を日付順のグラフとして出力させます。

image.png

まず、Chart.js のライブラリ本体をダウンロードします。

これを /wwwroot/js/lib へ配置し、/wwwroot/index.html にライブラリへの参照を追加します。

wwwroot/index.html
    <head>
       <meta charset="utf-8" />
       ... 中略
       <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
+      <script src="js/lib/Chart.min.js"></script>
    </head>

/wwwroot/js 配下に、Chart.js を呼び出す以下のモジュールを定義します。

chart-companion.js
export function createGraph(context, graph) {
   new Chart(context, graph)
}

Razor コンポーネント ファイル FetchData.razor にて IJSRuntime への参照と、グラフの出力先となる canvas 要素、要素への参照を格納する ElementReference 型のフィールドを追加します。

Pages/FetchData.razor
+   @inject IJSRuntime JSRuntime

    <h1>Weather forecast</h1>
    ... 中略

    @if (forecasts == null)
    {
       <p><em>Loading...</em></p>
    }
    else
    {
+      <canvas @ref="graphCanvas"></canvas>
+   
       <table class="table">
          <thead>
             <tr>
        ... 中略
    }

    @code {
       private WeatherForecast[] forecasts;
+      private ElementReference graphCanvas;

上記 Razor コンポーネントに対するコードビハインドとして、FetchData.razor.cs を作成します。

Pages/FetchData.razor.cs
namespace BlazorSamples.Client.Pages
{
   public partial class FetchData { }
}

Chart.js へ渡すパラメータと対応するデータクラスを定義します。

Pages/FetchData.razor.cs
public partial class FetchData
{
   class LineData
   {
      public string Label { get; set; }
      public IEnumerable<int> Data { get; set; }
      public double Tension { get; set; } = 0.5;
      public string BorderColor { get; set; }
   }

   class LineGraphData
   {
      public IEnumerable<string> Labels { get; set; }
      public IEnumerable<LineData> Datasets { get; set; }
   }

   class LineGraph
   {
      public string Type { get; } = "line";
      public LineGraphData Data { get; set; }
   }
}

次に、これらデータクラスを用いてパラメータを構成する処理を追加します。

Pages/FetchData.razor.cs
protected async void CreateGraph()
{
   // WeatherForecastService から得られたデータをグラフ出力用データに変換する
   var temparetures = new LineGraphData()
   {
      // 各データの日付をグラフの軸とする
      Labels = forecasts.Select(f => f.Date.ToShortDateString()),

      // 摂氏/華氏それぞれの気温情報をグラフの要素として指定する
      Datasets = new List<LineData>()
      {
         new LineData()
         {
            Label = "Temp. (C)",
            Data = forecasts.Select(f => f.TemperatureC),
            BorderColor = "coral",
         },
         new LineData()
         {
            Label = "Temp. (F)",
            Data = forecasts.Select(f => f.TemperatureF),
            BorderColor = "lightgreen",
         }
      }
   };

   // コンパニオン モジュールへデータを渡す
   var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
      "import", "./js/chart-companion.js"
   );

   await module.InvokeVoidAsync(
      "createGraph",
      graphCanvas,
      new LineGraph() { Data = temparetures }
   );
}

上記関数の呼び出しを、ページの初期化処理中に追加します。

Pages/FetchData.razor
    protected override async Task OnInitializedAsync()
    {
       forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
+      CreateGraph();
    }

以上の変更により、画面上へ以下の通りグラフが出力されるようになりました。

image.png

参考:

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

【JavaScript】基本的な書き方2(オブジェクト・DOM)

長くなったので分けました(前の記事)。
【JavaScript】基本的な書き方

オブジェクトの基本

オブジェクトとは変数と関数の集合体で、情報を入れられて管理するための箱のようなもの。

以下のように、配列の値に名前(キー)をつけて管理する。

const 配列 {
  キー: 値,
  キー: 値,
}

書き方の例。

main.js
const point = {
  x: 100, //100という値にxという名前をつけて配列に入れる
  y: 180, //180という値にyという名前をつけて配列に入れる
}

オブジェクトの一つ一つの要素をプロパティと呼ぶ。

オブジェクトのプロパティにアクセス

  • オブジェクトのプロパティは、配列.キー もしくは 配列['キー'] で表す。
  • すでにあるプロパティを記述すると値が上書きされ、存在しないプロパティを記述すると値が追加される。
  • プロパティを削除する時は、delete を使う。

書き方の例。

main.js
const point = {
  point.x = 120; //xの値を120に変更
  point.z = 90; //値の追加(新しいキーで値を設定)
  delete point.z = 90; //値の削除
}

配列の中で別の配列のオブジェクトを展開

配列を別の配列の中に展開する時に使うスプレッド構文(...配列名)は、オブジェクトを展開する時にも使える。

書き方の例。配列pointの中に、別の配列otherPropsを展開する。

main.js
const point = {
        x: 100,
        y: 180,
        ...otherProps, //otherProps...展開する配列
    }

また、分割代入やレスト公文も、オブジェクトでも同様に使うことができる。

オブジェクトのキーのみを取得(Object.keys)

Object.keys(配列)

書き方の例。オブジェクトのプロパティを列挙する。

main.js
const keys = Object.keys(point);

keys.forEach(key => {
console.log(`Key: ${key} Value: ${point[key]}`)
    })
//出力結果:Key: x Value: 100, ...

※文字列で取得されているので、配列.キー ではなく 配列['キー'] の書き方を使って文字列を入れる。

複数のオブジェクトを配列に入れて管理

書き方の例。pointsという配列を作り、その中に要素として複数のオブジェクトを入れる。

main.js
const points = [
  {x: 30, y: 20},
  {x: 10, y: 50},
  {x: 40, y: 40},
];

//※2番目のプロパティのyの値を出力(0番目から始まるので[1])
console.log(points[1].y); //出力結果:50

文字列の操作

文字数を取得

文字列を取得する時は、以下のように書く。

文字列.length

書き方の例。定数strの文字列'Hello!'の文字数を取得する。

main.js
const str = 'Hello!';
console.log(str.length); //出力結果:6

部分文字列を取得

部分的に文字列を取得する場合は、以下のように書く。

文字列.substring(開始位置, 終了位置)
//もしくは
文字列.substr(文字数)

書き方の例。定数strの文字列'Hello!'の2(3)番目から3(4)番目の文字列を取得する。

main.js
const str = 'Hello!';
console.log(str.substring(2, 4)); //出力結果:ll

//文字列に対して配列のような記法を使うと、個々の文字が取得できる
console.log(str[1]); //出力結果:e

配列の要素を文字列として結合(join)

()の中に文字列を入れると、その文字列を挟んで結合する。

配列.join(文字列)

書き方の例。配列dを"/"を挟んで文字列として結合。

main.js
const d = [2020, 12, 5]
console.log(d.join('/')); //出力結果:2020/12/5

文字列を分割(split)

文字列を指定した区切り文字のところで分割する。

文字列.split(分割する文字列)

書き方の例。定数の文字列を":"のところで分割し、その返り値を分割代入を使って別々の定数に代入。

main.js
const t = '23:45:17';

const [hour, minute, second] = t.split(":");

console.log(hour); //出力結果:23
console.log(minute); //出力結果:45
console.log(second); //出力結果:17

その他の文字列に関する命令

  • 文字列.replace(置換する文字列, 置換後の文字列) ... 文字列内の指定文字列を置換
  • 文字列.toUpperCase() ... 文字列を大文字に変換
  • 文字列.toLowerCase() ... 文字列を小文字に変換

数値の操作

四捨五入/切り上げ/切り捨て

  • round() ... 四捨五入
  • 定数.toFixed(桁数) ... 指定した桁数で四捨五入
  • ceil() ... 小数点以下を切り上げ
  • Math.floor() ... 小数点以下を切り捨て

合計と平均を求める

Javascriptでは数値の合計や平均を求める命令はないので、forEachを使う。

書き方の例。

main.js
const scores = [10, 3, 9]; //scoresという配列を設定

let sum = 0; // sumという変数を用意

scores.forEach(score => {
    sum += score;
})
//配列の合計を求める。forEachで配列scoresの全ての要素に対して処理をし、引数scoreとして配列scoresのそれぞれの値を渡して変数sumに足していく

const avg = sum /scores.length; 
//配列の平均を求める。lengthで求めた配列の要素数で合計を割る

console.log(sum); //出力結果:22
console.log(avg.toFixed(3)); //出力結果:7.333

ランダムな数値を生成

Math.random() ... 0以上1未満のランダムな数値を生成

0からnまでのランダムな整数値を生成。

Math.floor(Math.random() * (n + 1))

min から max までのランダムな整数値を生成。

Math.floor(Math.random() * (max + 1 - min)) + min

書き方の例。1から6までのランダムな整数を生成。

main.js
console.log(Math.floor(Math.random() * (6 + 1 - 1)) + 1);

日時のデータの操作

現在日時を取得

  • new Date() ... 現在日時()
  • getTime() ... 協定世界時(UTC)

特定の日時のデータを設定

new Date() に引数を渡す。(年と月は必須)

日時の一部をデータとして取り出す

  • getFullYear() ... 年
  • getMonth() ... 月(0〜11)
  • getDate() ... 日(1〜31)
  • getDay() ... 曜日(0〜6)
  • getHours() ... 時間(0〜23)
  • getMinutes() ... 分(0〜59)
  • getSeconds() ... 秒(0〜59)
  • getMilliseconds() ... ミリ秒(0〜999)

書き方の例。

main.js
const d = new Date(1990, 7, 15);

console.log(`${d.getMonth() + 1}${d.getDate()}`); // 出力結果:8月 15日

日時のデータを変更する

後から日付を操作したい場合は、set で始まる命令を使う。

書き方の例。dの日付のデータを10月2日に変更する。

main.js
const d = new Date(1990, 7, 15);

d.setMonth(9,2); //データを変更

console.log(`${d.getMonth() + 1}${d.getDate()}`); //出力結果:10月2日

書き方の例。dの日付のデータを3日後に変更する。

main.js
const d = new Date(1990, 7, 15);

d.setDate(d.getDate() + 3); //データを変更

console.log(`${d.getMonth() + 1}${d.getDate()}`); //出力結果:8月 18日

ダイアログを出す

警告のダイアログ

文字列と「OK」ボタンを表示する。

alert('文字列');

確認のダイアログ

文字列と「キャンセル」/「OK」ボタンを表示する。

confirm('文字列');

書き方の例。OKを押した場合は「削除しました」、キャンセルを押した場合は「削除しました」を出力する。

main.js
const answer = confirm('削除しますか?');

if (answer) {
    console.log('削除しました');
} else {
    console.log('キャンセルしました');
}

タイマー機能

一定時間ごとに特定の処理を繰り返す(setInterval)

setInterval(実行する関数, 処理間隔);

//止める時
clearInterval(タイマーの識別子);

書き方の例。現在時刻を表示する関数showTime()の処理を、1秒ごとに繰り返す。

main.js
function showTime() {
  console.log(new Date());
}

setInterval(showTime, 1000);
//関数の返り値ではなく関数を引数として渡すため、showTimeの後ろに"()"は付けない
//1000ミリ秒 = 1秒

さらに clearInterval() を使い、showTime()が3回実行された時に止める。

main.js
let i = 0; //カウンター用の定変

function showTime() {
  console.log(new Date());
  i++; //カウンターを増やす
  if (i > 2) {
    clearInterval(intervalid);
  } //iの値が2より大きくなったら処理が止まるようにする 
}

const intervalid = setInterval(showTime, 1000);
//setInterval()の返り値が必要になるので、それを定数intervalIdに代入(タイマーの識別子)

一定時間後に一度だけ処理をおこなう(setTimeout)

setTimeout(関数function, 一定時間の指定[, 引数1, 引数2, …])

書き方の例。1秒後に1度だけ実行する。

main.js
function showTime() {
  console.log(new Date());
}

setTimeout(showTime, 1000); //一度だけ実行

setTimeout()を連続して呼び出すことで、繰り返し処理を行うこともできる。

main.js
function showTime() {
        console.log(new Date());
        setTimeout(showTime, 1000); //連続して呼び出す
    }

showTime();

clearInterval() を使い、showTime()が3回実行された時に止める。

main.js
let i = 0; //カウンター用の定変

function showTime() {
  console.log(new Date());
  const timeoutId = setTimeout(showTime, 1000); //setInterval()の返り値が必要になるので、それを定数timeoutIdに代入(タイマーの識別子)
  i++; //カウンターを増やす
  if (i > 2) {
    clearTimeout(timeoutId); //setTimeout()の返り値である定数を入れる
  } //iの値が2より大きくなったら処理が止まるようにする 
}

showTime();

例外処理

例外処理とは、エラーなどの想定外の事態に対処する処理のこと。

try…catch文

try {
  処理; // 例外が起きそうな場所をtryで囲う
}
catch(e) { //catchに引数を渡すと、その例外の情報を扱うことができる
  例外が起きた時の処理;
}

書き方の例。エラーが起きたらエラー情報を出力し、止まらずに後の処理も実行する。

main.js
const name = 3;

try {
  console.log(name.toUpperCase()); //数値に対して文字列を扱う関数を実行しようとし、エラーが起きる
} catch (e) {
  console.log(e); //出力結果:TypeError: name.toUpperCase is not a function
}

console.log('Finish!'); //出力結果:Finish!

クラス

クラス(テンプレートのようなもの)を宣言し、それを使ってオブジェクトを生成し(インスタンス)利用することができる。

オブジェクトでは、プロパティの値として関数を持たせることもできる。(関数をプロパティの値にした場合、その関数をメソッドと呼ぶ)

クラスを宣言

class クラス名 {
  constructor(引数) {
    this.キー: 引数;
    this.キー: 値;
  }
    関数() { ... },
}

書き方の例。Postというクラスを宣言した後に、postsの中に2つのインスタンスを生成し、show関数を呼び出して出力する。

main.js
class Post { //クラス名の先頭は大文字
  constructor(text) { //プロパティを初期化
    this.text = text; //インスタンスによって異なる値を入れる場合、constructor()に引数を渡すようにする
    this.likeCount = 0; //同じオブジェクト内のプロパティにアクセスするにはthisを使う
  }
  show() {
    console.log(`${this.text} - ${this.likeCount}いいね`);
  }
}

const posts = [
  new Post('JavaScriptの勉強中...'),
  new Post('プログラミング楽しい!'),
]; //インスタンスを作成

posts[0].show(); //出力結果:JavaScriptの勉強中... - 0いいね
posts[1].show(); //出力結果:プログラミング楽しい! - 0いいね
}   

静的メソッド

静的メソッドとは、インスタンスを生成しなくてもオブジェクトから直接呼び出せるメソッドのこと。(読み取り専用の用途で使う)
this はインスタンス自身を表すため、静的メソッドで使うことはできない。

クラスから直接呼び出すには static を使う。

class クラス名 {
  static メソッド名(){
    処理
  }
}

書き方の例。クラス自体の説明を出力する。

main.js
class Post {
  static showInfo()  {
    console.log('Post class version 1.0');
  }
}
Post.showInfo(); //出力結果:Post class version 1.0

クラスを継承

親クラスの内容を引き継いだ子クラスを作成することを継承という。
クラスの継承を行うには extends を使用する。

class クラス名 extends 親クラス名 {
  小クラス独自の処理;
}

書き方の例。

main.js
//親クラス
class Post {
  constructor(text) {
    this.text = text;
    this.likeCount = 0;
  }
  show() {
    console.log(`${this.text} - ${this.likeCount} いいね`);
  }
  like() {
    this.likeCount++;
    this.show();
  }
}
//子クラス
class SponsoredPost extends Post {
  constructor(text, sponsor) {
    super(text); //親クラスのconstructor()を呼び出す
    this.sponsor = sponsor;
  }
  show() {
    super.show(); //親クラスのshow()メソッドを使う
    console.log(`... sponsored by ${this.sponsor}`);
  }
}

const posts = [
  new SponsoredPost('3分動画でマスターしよう', 'dotgate'),
];

posts[0].show(); //出力結果3分動画でマスターしよう - 0 いいね ... sponsored by dotgate
posts[0].like(); //出力結果3分動画でマスターしよう - 1 いいね ... sponsored by dotgate

※小クラスでthisを使うには、super() で親クラスのconstructor()を呼び出す。
※小クラスで親クラスのメソッドを使うには、super.メソッド名() のように書く。

DOMとは

DOM(Document Object Model)とは、JavaScriptでHTMLの要素を操作するための仕組みのこと。
DOM は document という特殊なオブジェクトで扱うことができる。

要素を取得するメソッド

  • .getElementById() ... 特定のid属性を持つ要素を取得
  • .querySelector() ... 特定のCSSセレクタの最初の1つの要素を取得
  • .querySelectorAll()[n] ... 特定のCSSセレクタのn番目の要素を取得
  • .getElementsByTagName() ... 特定のタグ名を持つ要素を取得
  • .getElementsByClassName() ... 特定のclass属性を持つ要素を取得
    • (以下は要素の内容の属性を表す)
    • .textContent ... 要素のテキスト情報を表す
    • .title ... 要素に関する補助的な情報(hoverすると表示される)を操作

書き方の例。#targetというid属性を持つ要素のテキストを変える。

main.js
document.getElementById('target').textContent = 'Changed!'; //idの頭に"#"は不要

すべてのp要素のテキストを「n番目のpです」に変える。(すべての要素を変えるには、forEach を使う)

main.js
function update() {
  document.querySelectorAll('section > p').forEach((p, index) => {
    p.textContent = `${index}番目のpです`;
});

DOM ツリーの階層関係から要素を取得

  • .childNodes() ... 全ての子Nodeを取得
    • .children() ... 要素Nodeのみ
  • .firstChild() ... 最初の子Nodeを取得
  • .lastChild() ... 最後の子Nodeを取得
  • .parentNode() ... 親Nodeを取得
  • .previousSibling() ... 1つ前の兄弟Nodeを取得
    • .previousElementSibling() ... 要素Nodeのみ
  • .nextSibling() ... 1つ後の兄弟Nodeを取得
    • .nextElementSibling() ... 要素Nodeのみ

イベントに合わせて実行(addEventListener)

イベントに合わせて実行させる関数を登録するためのメソッド。

element.addEventListener(イベント, 関数, オプション);

書き方の例。buttonをクリックすると#targetのテキストが変わるようにする。

main.js
document.querySelector('button').addEventListener('click', () => {
  document.getElementById('target').textContent = 'Changed!';
  }
);

イベントの種類

  • click ... クリック
  • dblclick ... ダブルクリック
  • mousemove ... マウスを動かす
  • keydown ... キーボードのキーを押す

マウスカーソルの座標を取得(mousemove)

関数に引数を渡すと、ブラウザがイベントに関する情報をセットして渡してくれる。
引数はイベントオブジェクトと呼ばれ、慣習的に"e"がよく使われる。

書き方の例。マウスカーソルのX座標とY座標を出力。

main.js
document.addEventListener('mousemove', e => {
  console.log(e.clientX,e.clientY);
});

キーボードで押されたボタンを取得(keydown)

書き方の例。キーボードで押したキーを出力。

main.js
document.addEventListener('keydown', e => {
  console.log(e.key);
});

フォームに関するイベントの種類

  • focus ... フォーカス
  • blur ... フォーカスを外す
  • input ... 内容が更新
  • change ... 更新が確定
  • submit ... 送信

入力されたテキストの文字数を表示(input)

main.js
const text = document.querySelector('textarea');

text.addEventListener('input', () => {
  console.log(text.value.length);
})

送信された時にメッセージを出力(submit)

書き方の例。
※フォームを送信するとページ遷移が発生するためテキストが一瞬で消えてしまうので、既存の動作をキャンセルする必要がある。

main.js
document.querySelector('form').addEventListener('submit', e => { //Eventオブジェクト"e"を渡し、
  e.preventDefault(); //既存の動作をキャンセル
  console.log('submit');
});

.preventDefault() ... 既存の動作をキャンセル
※clickイベントでも表示させることは可能だが、formタグを使っておくと、Enterキーでもフォームを送信できるというメリットがある。

イベントの伝播

入れ子になった要素において、イベントが順々に伝播する。

例。クリックするたびに打ち消し線が付いたり外れたりする。
Eventオブジェクトを使えばクリックした要素は"e.target"、EventListenerを追加した要素は"e.currentTarget"で取得できる。

index.html
<ul> <!-- e.currentTarget -->
  <li>Todo</li>
  <li>Todo</li> <!-- e.target -->
  <li>Todo</li>
</ul>
style.css
li.done {
  text-decoration: line-through;
}
main.js
document.querySelector('ul').addEventListener('click', e => {
  if (e.target.nodeName === 'LI') {
    e.target.classList.toggle('done');
  }
});

style属性・classの操作

style属性の操作

styleの指定をする場合は、.style の後に続けて書く。

element.style.プロパティ = 値;

書き方の例。取得する要素を定数にし、スタイルを指定する。

main.js
document.querySelector('button').addEventListener('click', () => {
  const targetNode = document.getElementById('target');

  targetNode.style.color = 'red';
  targetNode.style.backgroundColor = 'blue';
  }
);

※ハイフン(-)を含むstyleプロパティの場合は、繋げて次の単語の頭文字を大文字にする。

※CSSとの役割分担を明確にするため、見た目の指定はCSSに任せて、JavaScriptではclass属性の操作だけを書くようにする。

classの操作(classList)

classをつけたり外したりなどの指定をする場合は、.classList を使う。

element.classList.add('クラス名');
  • .classList
    • .add() ... クラス属性を追加する
    • .remove() ... クラス属性を外す
    • .toggle() ... クラス属性をが付いていなかったら追加する、付いていたら外す
    • .contains() ... 特定のクラスが付いているかどうかtrue/falseで返す
  • .className = 'クラス名'; ... クラス属性を書き換える(もともとついているクラスは外れる)

書き方の例。buttonをクリックすると、#targetに.my-colorというclass属性がついていなかったら追加する、ついていたら外すという処理をするようにする。

main.js
document.querySelector('button').addEventListener('click', () => {
  const targetNode = document.getElementById('target');

  if (targetNode.classList.contains('my-color') === true) {
    targetNode.classList.remove('my-color');
  } else {
    targetNode.classList.add('my-color');
  }
});

上記のif文は .toggle を使うと下記のように短く表せる。

main.js
    targetNode.classList.toggle('my-color');

カスタムデータ属性(data-*)

HTMLでは独自の属性を設定できるので、その値をJavaScriptで取得して操作する。
※カスタムデータ属性の名前は data- で始まる。

まずHTMLでカスタムデータ属性を記述。

<要素 data-***="値">テキスト</要素>

JavaScriptでカスタムデータ属性の取得。

const 定数名 = document.querySelector('要素名');

値を取得する時は、以下のように置き換える。
data-***dataset.***
※datasetというプロパティが、その要素が持つすべてのdata属性の集まり。

console.log(定数名.dataset.***);

書き方の例。data-nameというデータ属性にTitleという値を設定。

index.html
<h1 id="target" data-name="Title">タイトルだよ</h1>

buttonをクリックすると、#targetのテキスト内容がデータ属性"data-name"の値に変わるようにする。

main.js
document.querySelector('button').addEventListener('click', () => {
  const targetNode = document.getElementById('target');

  targetNode.textContent = targetNode.dataset.name; //data-name -> dataset.name
});

要素をDOMに追加・削除

要素をDOMに追加

JavaScriptでHTML要素を生成する時は、.createElement() を使う。

const 定数名(作成する要素名) = document.createElement('HTML要素');

子要素の末尾に追加する時は .appendChild() を使う。

親要素.appendChild('作成した要素名');

書き方の例。buttonをクリックすると、ulの子要素の末尾にli要素の"items 2"が追加されるようにする。

main.js
document.querySelector('button').addEventListener('click', () => {
  const item2 = document.createElement('li'); //li要素を作る
  item2.textContent = 'item 2'; //中身のテキストを設定する

  const ul = document.querySelector('ul'); //親要素を取得
  ul.appendChild(item2); //親要素に対して子要素の末尾に追加
});

要素を複製してDOMに追加

要素を複製する時は .cloneNode を使う。

const 定数名(複製して作られる要素名) = element.cloneNode(true);

1つ前に追加する時は .insertBefore() を使う。

親要素.insertBefore(追加する要素, 追加する後ろの要素);

書き方の例。

main.js
document.querySelector('button').addEventListener('click', () => {
  const item0 = document.querySelectorAll('li')[0]; //HTML要素の1つめのli要素にitem0と名前をつける
  const copy = item0.cloneNode(true); //item0を複製

  const ul = document.querySelector('ul'); //親要素を取得
  const item2 = document.querySelectorAll('li')[2];//1つ前の要素を取得
  ul.insertBefore(copy, item2); //copyをitem2の前に追加
});

DOMから要素を削除

削除する要素.remove();

ただし上記はIEだと効かず、IE対応するなら以下を使う。

親要素.removeChild('削除する要素');

フォームから値を取得

テキストボックスから値を取得

inputで入力された値は .value で取得できる。

const 定数名 = document.getElementById("定数名").value;

書き方の例。テキストボックスで入力を受け取り、list要素を作成する。

main.js
document.querySelector('button').addEventListener('click', () => {
  const li = document.createElement('li'); //リストに追加するli要素(定数名:li)を作成
  const text = document.querySelector('input'); //input要素を取得
  li.textContent = text.value; //inputで入力された値を取得
  document.querySelector('ul').appendChild(li); //親要素に対して子要素の末尾にliを追加 ※liは定数名なので""で囲わない
});

セレクトボックスから値を取得

  • 選択された値は .value で取得。(タグの中身が使われるが、別の値にしたい場合はHTMLでvalue属性を指定)
  • .selectedIndex はindex番号を表す。

書き方の例。セレクトボックスで選択された値を受け取り、list要素を作成する。

main.js
document.querySelector('button').addEventListener('click', () => {
  const li = document.createElement('li'); //li要素を作成
  const color = document.querySelector('select'); //select要素を取得してcolorという定数名をつける
  li.textContent = `${color.value} - ${color.selectedIndex}`; //valueプロパティ、selectedIndexプロパティのそれぞれで選択された値を表示
  document.querySelector('ul').appendChild(li); //親要素を取得して子要素の末尾にliを追加
});

ラジオボタンから値を取得

書き方の例。ラジオボタンで選択された値を受け取り、list要素を作成する。

main.js
document.querySelector('button').addEventListener('click', () => {
  const colors = document.querySelectorAll('input'); //すべてのinput要素を取得
  let selectedColor; //選択された値を保持するため、変数を宣言

  colors.forEach(color => {
    if (color.checked === true) {
      selectedColor = color.value;
    } //要素がチェックされていたら、その値をselectedColorに代入
  });

  const li = document.createElement('li'); //リストに追加するli要素(定数名:li)を作成
  li.textContent = selectedColor; //liに選択された値を入力
  document.querySelector('ul').appendChild(li); //親要素を取得して子要素の末尾にliを追加
});

チェックボックスから値を取得

チェックボックスで入力された値は .checked で取得できる。

書き方の例。チェックボックスで選択された値を受け取り、list要素を作成する。
チェックボックスは複数選択可なので、選択された値を配列で保持する。

main.js
document.querySelector('button').addEventListener('click', () => {
  const colors = document.querySelectorAll('input'); //すべてのinput要素を習得
  const selectedColors = []; //選択された値を配列で保持

  colors.forEach(color => {
    if (color.checked === true) {
      selectedColors.push(color.value); //push() ... 配列の先頭に要素を追加                
    }//チェックされていたら、チェックされている要素の値を配列selectedColorsに追加
  });

  const li = document.createElement('li'); //リストに追加するli要素(定数名:li)を作成
  li.textContent = selectedColors; //※デフォルトでカンマ区切りになるので、".join(',')"は省略可能 
  document.querySelector('ul').appendChild(li); //親要素を取得して子要素の末尾にliを追加
});

最後に

この辺りをちょっとわかるようになってから、コードを見たときに何が書いてあるのかが前よりだいぶ理解できるようになったので、とても楽しいです。(まだ曖昧な理解の箇所もあるけれど...)
練習にいろいろ作ってみて慣れていこうと思います。

参考にさせてもらったサイトなど

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

ReactとPHP 初めての連携

はじめに

今回はReactで簡単なフォームを作って、PHPの方で受け取るプログラムを書いてみました。Laravelではやったことがあるのですが、フレームワーク無しのPHPでやったことがなくて、フレームワーク無しのPHPをだいぶ忘れてしまったのでやってみました。

テキストを入力して送信したら、そのテキストが表示されて、何も入力しなければエラーが表示される感じのシンプルなフォームです。
image.pngimage.png

以下を参考にさせていただきました。
参考:Create a Contact Form With PHP and React in 3 Min

React側

package.json
{
  "name": "simple-form",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "@material-ui/core": "^4.11.2",
    "@types/axios": "^0.14.0",
    "@types/node": "^14.14.12",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "axios": "^0.21.0",
    "react": "^16.11.0",
    "react-dom": "^16.11.0",
    "react-hook-form": "^6.13.0",
    "react-scripts": "^4.0.1",
    "styled-components": "^5.2.1",
    "ts-node": "^9.1.1",
    "typescript": "^4.1.2"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}

↑使用したpackageです。react-hook-formとmaterial-uiを使用していますが、これらの説明は割愛します。


↓react-hook-formの記事も書いているので良ければ是非!
react-hook-formの使い方を解説!V6.12.0追加

index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";

ReactDOM.render(<App />, document.getElementById("root"));

index.tsxでApp.tsxからインポートしています。見慣れた形だと思います。

import React, { useState } from "react";
import axios from "axios";
import { Button, CircularProgress, styled, TextField } from "@material-ui/core";
import { useForm } from "react-hook-form";

type FormData = {
  text: string;
};

export const App: React.FC = () => {
  const [message, setMessage] = useState("");
  const [error, setError] = useState(false);

  const {
    register,
    handleSubmit,
    formState: { isSubmitting },
  } = useForm<FormData>();

  const onSubmit = async (data: FormData) => {
    try {
      const res = await axios.post("http://localhost:8080/index.php", data);
      setError(res.data.error);
      setMessage(res.data.message);
    } catch {
      setError(true);
      setMessage("通信に失敗しました。");
    }
  };

  return (
    <>
      <Form onSubmit={handleSubmit(onSubmit)}>
        <TextField
          defaultValue=""
          margin="normal"
          variant="outlined"
          name="text"
          error={error}
          inputRef={register}
          helperText={message}
        />
        <Button
          type="submit"
          variant="contained"
          color="primary"
          disabled={isSubmitting}>
          {isSubmitting ? <CircularProgress size={24} /> : "送信"}
        </Button>
      </Form>
    </>
  );
};

const Form = styled("form")({
  display: "flex",
  flexDirection: "column",
  width: 300,
  margin: "0 auto",
});

axiosでhttp://localhost:8080/index.phpにフォームの値を送信し、レスポンスとして、errorとmessageが入った連想配列を受け取ります。PHPの方のコードを見ればイメージがしやすいと思います。

PHP側

PHPはDockerでnginxを使い、http://localhost:8080で立ち上げました。MAMPとかを使っても簡単にできると思います。

index.php
<?php
header("Access-Control-Allow-Origin: *");
header('Access-Control-Allow-Headers: Content-Type');
$rest_json = file_get_contents("php://input"); // JSONでPOSTされたデータを取り出す
$_POST = json_decode($rest_json, true); // JSON文字列をデコード

if(empty($_POST['text'])) {
    echo json_encode(
        [
           "error" => true,
           "message" => "Error: 入力してください。",
        ]
    ); 
} else {
    echo json_encode(
        [
           "error" => false,
           "message" => 'Success: 入力されたテキスト→'.$_POST['text'],
        ]
    ); 
}
  • Access-Control-Allow-Originで異なるオリジンからのアクセスを可能に
  • Access-Control-Allow-Headersで使用可能なHTTPヘッダーを設定
  • file_get_contentsでJSONでPOSTされたデータを取り出す
  • POSTされたデータがなければ、errorがtrueでエラーメッセージを返し、POSTされたデータがあれば、errorがfalseでPOSTされたデータを返す感じです

終わりに

ここまで読んでいただきありがとうございます!細かい説明があまりできていませんが、初めてフロントエンドとサーバーサイドで連携する時のサンプルとしてみていただければと思います。次はデータベースを使って簡単なCRUDをできるようにしたいなと思います。文章力あまりないのでわかりにくいかもしれませんが、少しずつ続けながら上げていきたいと思います。

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

Gatsbyでフォントをセルフホスティングする方法

はじめに

GatsbyでWebフォントをセルフホスティングする方法を紹介します。
セルフホスティングをすると、Google Fontsなどのサービスからフォントを取得する場合と比べて、パフォーマンスが向上するというメリットがあります。

フォントのセルフホスティングとは

セルフホスティングとは、自前のサーバに置いたフォントファイルをwebページのフォントとして使用することです。
Google Fontsなどのサービスがもつサーバからフォントを取得する方法とは異なります。

セルフホスティングするメリット

セルフホスティングをするメリットは、Webページのパフォーマンスが向上することです。
サービスがもつサーバからWEBフォントを取得する場合は、そのサーバにHTTPリクエストをする必要があり、日本語などの文字の種類が多いフォントの場合は、大きなボトルネックになってしまいます。
しかしながら、セルフホスティングをすると、上記のフォント取得による遅延を軽減することができます。

使用するパッケージ

Fontsourceというモノレポから好きなフォントを選び、インストールします。
自前でフォントファイルを、デュレクトリに配置するという方法もありますが、設定が面倒臭いのでライブラリに頼ることとします。

実装方法

Google Fontsにも存在する「Noto Sans JP」をインストールすることを例に実装方法を説明します。

パッケージのインストール

まずパッケージをインストールします。

yarn add fontsource-open-sans

パッケージマネージャーにnpmを使用している場合は下記の通りです。

npm install fontsource-open-sans

Layout.jsにimportする

Layout.jsに下記のようにimportします。

import 'fontsource-noto-sans-jp';

上記では、デフォルトのフォントウェイトに、全てのスタイル(Boldなど)が含まれる状態でimportしてしまうため、下記のように使用するフォントウェイトとスタイルを指定すると、ペイロードサイズを削減できます。

import "fontsource-noto-sans-jp/700.css"
import "fontsource-noto-sans-jp/900-normal.css"

あとは、CSSでfont-familyを指定してあげれば、使用ができるようになります。

まとめ

Fontsourceというライブラリを使用して、GatsbyでWebフォントをセルフホスティングする方法を紹介しました。
参考になれば幸いです!

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

【JS】通貨記号&カンマ付き文字列をNumberに変換する

replace で変換してNumberに変換します。

> Number("¥1,000,000".replace(/[¥,]/g, ''))
  => 1000000

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/replace

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

リモートワーク中、家族に画面を覗かれて変な空気になったので、Chrome拡張機能を開発している

DMMアドベントカレンダー18日目の記事です。
昨日は、@matsuda-hiroki さんの「なにもしてないのにEC2インスタンスが壊れた」でした。

奥さん「何見てるの?」

こんにちは。DMM.comの@tmitsuoka0423です。

今年4月に電子書籍事業部にジョインし、ずっと自宅でリモートワークをしています。
電子書籍事業部では、一般向け電子書籍サービスであるDMM電子書籍と、成人向け電子書籍サービスFANZA電子書籍の両方の開発を行っています。

そんなある日、悲劇は突然起こりました。

アダルトコミック画面の機能を開発していた夕方のこと。
奥さんが仕事から帰ってくるなり、私のディスプレイを見て言いました。「何見てるの?」

リモートワーク中はディスプレイの覗き込みに注意

私は真剣に仕事をしていただけですが、家族から見るとそうは見えないみたいです。
(仕事内容は事前に伝えているのに)
お子さんのいる方・二世帯住宅にお住まいの方はもっと注意が必要になるのではないでしょうか。

Chrome拡張機能「tile」を開発中

そんなリモートワーカーのために、Chrome拡張機能を開発しています。

スクリーンショット_2020_12_15_12_52.png
HTML上の画像にぼかしを入れてくれます。

完成次第、公開します!

使い方イメージ

1.インストール後、Chrome右上のパズルマークをクリック、tileをクリックします。

2020-12-12_15h28_31.png

2.ぼかすを選択します。

2020-12-12_15h31_30.png

3.ページに表示されている画像にぼかしが入ります。

スクリーンショット_2020_12_15_12_52.png

内部処理の概要

内部で行っていることはとてもシンプルです。
全てのimgタグを取得し、CSS関数のblurを利用してぼかしをかけています。

const imgList = document.getElementsByTagName("img");
for (let i = 0; i < imgList.length; i++) {
  imgList[i].style.cssText = "filter: blur(8px);";
}

上記のコードを利用して、拡張機能をインストールせずに、Chrome DevToolsConsoleタブから実行することも可能です。

スクリーンショット_2020_12_15_12_59.png

まとめ

開発中のChrome拡張機能「tile」の紹介しました。
リモートワークをより働きやすくするためのアプリを今後も開発していきます。

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

NuxtアプリをSSGでビルドしてCI/CDをお手軽に設定する【Github Actions × Firebase Hosting】

はじめに

今回はNuxt.jsのSSG(静的サイトジェネレーター)モードで作成したアプリのCI/CDを設定する方法を紹介します!
Github ActionsとFirebase Hostingを連携させるとかなり手軽に設定できたのでNuxt/Vue初心者の方も実践できる内容だと思います。

対象読者

・実務でCI/CDを使った事が無い人
・CI/CDという言葉は聞いた事があるがよく分からない人
・Vue/Nuxtを触った事はあるがレンダリングの仕方までは気にした事がない (SSGってナニ?な人)
・firebaseがなんとなく分かる人

説明しない事

・firebase各種サービスの細かな説明
・テスト(Jest)の書き方

SSG (静的サイトジェネレーター)とは?

個人的にはこの記事の説明が分かりやすいと思います
Nuxt.jsを使うときに、SPA・SSR・SSGのどれがいいか迷ったら -Qiita

NuxtをSSGモードで始めると、ビルド時(nuxt generate実行時)にvueファイル内で呼ばれているAPIからデータを取得し、それに基づいたHTMLが生成されます。

ビルド時点のDBから取得した値がHTMLへ直書きされるので、DBの値が変わったとしても再度デプロイするまではHTML内の値が変わる事はありません。

特徴

・サーバーサイドとデータのやりとりをする必要が無いので
 1) レスポンスが速い
 2) セキュリティ面のリスクが軽減される

・データの更新をする為には都度ビルドする必要があるので、頻繁に更新するサイトには向かない (LPやポートフォリオには向いている)

CI/CDとは

ニフクラさんによると

「CI」とは「Continuous Integration(継続的インテグレーション)」の略で、ソフトウェア開発におけるビルドやテストを自動化し、継続的に行うアプローチのことです。
「CD」とは「Continuous Delivery(継続的デリバリー)」の略で、CIによってテストされたコードのマージや、本番環境向けのビルドの作成を自動的に行い、本番環境にデプロイが可能な状態を整えるプロセスのことです。

CI/CDとは | ニフクラ

簡単に言えば、CI/CDとは継続的にビルド/テスト/デプロイを行う事で、いつでも本番環境を更新できるような状態に保つ為の仕組みの事です。
近年の開発手法の主流である「アジャイル開発」の中で、リリース回数が多くなっても品質を下げない様にする為の施策であるとも言えます。

今回実装するCI/CDの動き

今回作成するシステムは以下の様になります
Untitled Diagram (1).png
mainブランチはリリース用のブランチなので、そこで作業することはありません。

手順
1. 機能を実装し、実装箇所のテストを書いたら、mainブランチへコードをpushします。
2. Github Actionsはmainブランチへのpushを検知して、ビルド、テストを実行します。
3. 2で問題がなければそのままFirebase Hostingへコードをデプロイします。
4. 問題があった場合(テスト/ビルドに失敗)は通知がされ、本番への反映はされません。

本編

※ Node.jsがインストールされていること、Githubが使える事が前提

環境

Node v12.20.0
Nuxt v2.14.6
firebase v8.1.1


それでは実際に環境構築していこうと思います!
まず最初にgithubでリポジトリを作成して、cloneしておきます。

$ git clone[作成したリポジトリ名]
$ cd[作成したリポジトリ名]


Nuxtアプリの作成

次にnuxtアプリを作成します。

npx create-nuxt-app [アプリ名]

いくつか質問に答えます。

create-nuxt-app v3.4.0
✨  Generating Nuxt.js project in '[アプリ名]'
? Project name: '[アプリ名]'
? Programming language: JavaScript
? Package manager: Npm
? UI framework: None
? Nuxt.js modules: Axios
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: jsconfig.json
? Continuous integration: None
? Version control system: None

create-nuxt-appが完了したら、生成したファイルをルートディレクトリに移動します。

# ファイルをルートに移動
mv [アプリ名]/{*,.*} .
# 空になったディレクトリを削除
rm -rf [アプリ名]

最後にfirebaseのパッケージをインストールします。
npm i firebase

firebaseのセットアップ

プロジェクトの作成

https://console.firebase.google.com

firebaseコンソールにてプロジェクトを作成してください。
初めての方はGmailのアカウントが必要になります。
無事作成できると、コンソールページに入れるようになります。

スクリーンショット 2020-12-11 20.20.40.png

Cloud Firestoreの設定

次にCloud Firestoreのページへ移動し、データベースを作成します。
今回はテストモードで開始しますが、運用の際にはルールを設定してください。
image.png

データベースが作成されたら、適当なレコードを登録しておきます。
自分はusersコレクションにnameを持ったドキュメントをいくつか作成しました。

スクリーンショット 2020-12-11 20.25.43.png

これが今回扱うデータになります。

アプリにFirebaseを追加

自分のアプリ(今回はNuxt)でfirebaseを使うための設定をしていきます。
「プロジェクトを設定」から「ウェブアプリに Firebase を追加」を選択します。

手順が表示されるので、この通りに進めていきます。

image.png

 1.「このアプリのFirebase Hostingも設定します」にチェックを入れてください。
 2. モジュールバンドラを使用するので「FirebaseSDKの追加」は飛ばしてください。
 4. firebase initでいくつかの質問を答える事になります。以下の様に回答してください。

? Which Firebase CLI features do you want to set up for this folder?:
 > firestore/hosting
? Please select an option: 
 > Use an existing project
Select a default Firebase project for this directory:
 > [先ほど作成したプロジェクト]
? What file should be used for Firestore Rules?
 > firestore.rules
? What file should be used for Firestore indexes? 
 > firestore.indexes.json
What do you want to use as your public directory? 
 > dist
? Configure as a single-page app (rewrite all urls to /index.html)? 
 > No
? Set up automatic builds and deploys with GitHub? 
 > Yes # Githubの認証が必要になります
? For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) 
 > [今回使っているリポジトリ]
? Set up the workflow to run a build script before every deploy? 
 > Yes
? What script should be run before every deploy?
 > npm ci & npm run build
? Set up automatic deployment to your sites live channel when a PR is merged?
 > Yes
? What is the name of the GitHub branch associated with your sites live channel?
  > main

後半はCI/CDの設定についてです (後述)
最後にfirebase deploy --only hosting:[アプリ名]を実行とありますが、これはデプロイ用のコマンドであり、今のままではうまくいきません。後ほど設定をします。

テストの確認

まずデプロイの前に、実装したコードに不備が無いかテストを実行します。
デフォルトでtestディレクトリに Logo.spec.js があると思います。
今回は説明の為にこのファイルのみテストします。

Logo.vueがvue instanceであるかのチェックです。
npm run test を実行すればテストが実行されます。

image.png

jestを使ってテストをしています。
1 passed, 1 total とあるように、合計1つのテストケースのうち、1つのテストがpass(成功)しました。
コードorテストに不備があるとエラーが起きます。

このようにしてデプロイ(本番反映)前にテストを実行する事でバグを未然に防げます。ただし、テストを実行し忘れてデプロイしてしまう可能性もあるので、CI/CDを使ってデプロイの度に自動テストをする仕組みを作成します。


本番環境へデプロイ

テストが成功した事を確認したら、本番環境へデプロイしていきます。

まずビルド用のコマンド、

npm run generate

を実行します。

するとdist配下に静的ファイルが作成されます。
このdistの中身が、今回Firebase Hostingによってホスティングされる静的ファイル群です。

ビルドが成功したら、デプロイ用のコマンド、

firebase deploy --only hosting

を実行してみてください。

完了するとHosting URL:[URL]と最後に表示されるので、アクセスしてみます。
image.png

無事アクセスできました!

Nuxt × firestoreでSSGを試す

それではNuxt側でfirestoreにアクセスし、データを取得/表示してみます。

firebaseSDKを使う

まずはfirebaseSDKをNuxtで使うために、firebaseを初期化するためのpluginファイルを作成します。

plugins配下にfirebase.jsを作成します。
firebaseConfigの部分は自分のものに変えてください。


firebaseConfigの取得方法
コンソールにログインして、
「プロジェクトを設定」→ 「マイアプリ」 → 「Firebase SDK snippet」
と進み、「CDN」を選択するとコードが表示されます。
firebaseConfigの部分だけコピーします。
スクリーンショット 2020-12-11 21.42.23.png


plugins/firebase.js
import firebase from 'firebase'

const firebaseConfig = {
  apiKey: 'Your apiKey',
  authDomain: 'Your authDomain',
  projectId: 'Your projectId',
  storageBucket: 'Your storageBucket',
  messagingSenderId: 'Your messagingSenderId',
  appId: 'Your appId',
}

if (!firebase.apps.length) {
  firebase.initializeApp(firebaseConfig)
}

export default firebase



これ以降はNuxt側で

import firebase from '~/plugins/firebase'

と書く事で、先ほど作成したfirebaseプロジェクトを利用できるようになります!

asyncDataとmountedを比較

SSGでレンダリングする場合には、asyncData内でAPIへリクエストを送り、レスポンスのPromiseを{key:value}形式で返します。
keyにはtemplate内で使いたい名前を、valueにはPromiseを入れます。

実際にindex.vueを書き換えて、SSGとSPAでの挙動の違いを確認してみます。

index.vue
<template>
  <div class="container">
    <div>
      <h2>Users with asyncData (SSG)</h2>
      <p v-for="(user, i) in asyncUsers" :key="'user-async' + i">
        {{ user.name }}
      </p>

      <h2>Users with mounted</h2>
      <p v-for="(user, i) in mountedUsers" :key="'user-mounted' + i">
        {{ user.name }}
      </p>
    </div>
  </div>
</template>

<script>
import firebase from '~/plugins/firebase'
export default {
  data() {
    return {
      mountedUsers: [],
    }
  },
  async asyncData() {
    const res = await firebase
      .firestore()
      .collection('users')
      .get()
      .then((querySnapshot) => {
        const array = []
        querySnapshot.forEach((doc) => {
          array.push(doc.data())
        })
        return array
      })
      .catch((e) => console.log(e))
    return { asyncUsers: res }
  },
  async mounted() {
    await firebase
      .firestore()
      .collection('users')
      .get()
      .then((querySnapshot) => {
        querySnapshot.forEach((doc) => {
          this.mountedUsers.push(doc.data())
        })
      })
      .catch((e) => console.log(e))
  },
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

h2 {
  margin: 16px 5px;
}
p {
  margin: 5px 0px;
}
</style>



npm run devで確認してみます。

asyncDataはビルド時に実行されていて、DBから取得した値がHTMLに直書きされている状態なので、その都度取得するmountedより表示速度が速い事が分かります。

test666.gif

SSGを利用している部分をデプロイ

今回新しい実装をしたので、再度本番環境へ反映させます。

手順としてはざっくり、

テストの実行 (本来なら変更箇所のテストを追加作成) → npm run generate
ビルドの実行npm run generate
デプロイの実行firebase deploy --only hosting

となると思います。

ただし、
毎回これらの手順を人の手で実行するのは面倒である事はもちろん、テストを実行し忘れたままデプロイしてしまう等、本番環境での不具合にも繋がります。

本番環境への反映をシステム化する為にもCI/CDを導入します。

以上の点を踏まえて、CI/CDを設定して変更内容をデプロイします!

CI/CDで自動テスト&自動デプロイをする

デプロイの設定は既に完了しています。

firebase init

を実行した時にいくつか質問に答えたと思います。
この質問の後半での回答がCI/CDの設定になります。

? Set up automatic builds and deploys with GitHub? 
 > Yes
? For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) 
 > [今回使っているリポジトリ] #Github actionsを設定するリポジトリ
? Set up the workflow to run a build script before every deploy? 
 > Yes
? What script should be run before every deploy?
 > npm ci & npm run build #デプロイ時に実行するコマンドを指定
? Set up automatic deployment to your sites live channel when a PR is merged?
 > Yes # マージされた時も自動デプロイを有効にするかどうか
? What is the name of the GitHub branch associated with your sites live channel?
  > main #ここで指定したブランチへpushした時にGithub actionsが走ります

mainブランチへのpushをGithub actionsが検知し、指定したJobを実行してくれるようになります。

ビルド、テストの設定についてはもう少し手を加える必要があります。

では、実行するJobを編集していきます。
CI/CDの定義はymlファイルに記述します。

既存のnpm ci & npm run buildを削除して、以下のように編集します。

.github/workflows/firebase-hosting-merge.yml
-      - run: npm ci & npm run build
+      - name: Cache dependencies
+        uses: actions/cache@v1
+        with:
+          path: ~/.npm
+          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+          restore-keys: |
+            ${{ runner.os }}-node-
+
+      - name: Install dependencies
+        run: npm ci
+        
+      - name: Run test
+        run: npm test
+        
+      - run: npm run generate

それでは、変更内容をmainブラントへpushしてみましょう!

pushしたらgithub actionsをみてみます。
リポジトリのActionsタブにワークフローが表示されていると思います。

image.png


詳細をみてみると、先ほど定義したjobが実行されている事が分かります。

image.png



ステータスが緑のチェックになっていれば成功です!
image.png
これ以降はmainブランチに変更内容をpushするだけで、本番環境へ反映されるようになります。



ここでテストが通らなければデプロイに失敗します。

例えば以下のように変更すればテストが通らず、デプロイにも失敗します。

- expect(wrapper.vm).toBeTruthy()
+ expect(wrapper.vm).toBeFalsy()

まとめ

今回扱った技術については理解が曖昧な部分があったので、自分としてもまとめられて良かったです!
何よりSSGが使いやすくて好きになりました!
ポートフォリオサイトに最適だと思ったので、今回の構成で作り直してみようと思います。
SSR/ISRも試してみよう。


22卒の学生エンジニアです。
Twitterもやってるのでフォローお願いします!
https://twitter.com/1keiuu

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

HOYAのVoiceTextを使ってDiscordのテキストを読み上げるBotを作ってみた

はじめに

このBotを作った経緯としては、夜遅くなどどうしてもしゃべれない時間帯があるため、そうした時間帯でもVCで参加する方法が必要になり、このBotを作ってみました。
どうやら、既に喋太郎というBotがあるらしいですが、このBotを最初に作った時(2018年2月ぐらい)にはなかったのでご容赦ください。
似たようなことをしたい人の参考になれば良いなと思います。

ソースコード

GitHubにあります。
noriokun4649/Discord-TTS-Voice-channel-Bot
利用してるライブラリなどの依存関係はpackage.jsonに書いてありますが下記の通りです。

  • Node.js v12.19.0
  • @discordjs/opus : 0.3.2
  • config : 3.3.2
  • config-reloadable : 1.0.8
  • discord.js : 12.4.0
  • ffmpeg-static : 4.2.7
  • voice-text : 0.1.2

開発してみての感想

VoiceTextのライブラリや、Discordのライブラリが用意されていたため、簡単に開発することができました。
躓いた点としては、VoiceTextのライブラリから返ってくるbufferをDiscord.jsのstreamに渡す方法に躓きました。
このBotを作った当初(2018年2月ごろ)の時点では、一度wavファイルに保存して、保存後wavファイルを読み取りDiscord.jsに渡すという糞みたいな処理をしてましたが、現在は直接bufferからstreamに変換して渡しています。

また、DiscordのAPI自体の仕様で、Discordのテキストチャネルで送信されるサーバ独自の絵文字やメンションなどが変わった形で送られてくるため、こうした絵文字やメンションの処理も少しだけ戸惑いましたが、最終的には正規表現で対応しました。

使い方

Node.jsで動くので通常どおりnpmのコマンドで使えます。

機能や設定、コマンドなど

機能はそんなに多くありませんが下記のような機能があります。

  • コマンドの接頭語の変更機能
  • エラー時の自動再起動する機能
  • 読み上げ音声の変更機能
    • 音声の種類変更
    • 読み上げ音声の速度の変更
    • 読み上げ音声の高さの変更
  • ブラックリスト
    • コマンドの接頭語別に読み上げ禁止にする機能
    • ユーザー別に読み上げ禁止にする機能
    • すべてのBotを読み上げ禁止にする機能

読み上げ音声の変更機能以外は、Configファイルで設定可能です。

Configファイルについて

configフォルダ内のdefault.jsonがコンフィグファイルです。

{
  "Api": {
    "discordToken": "",
    "voiceTextApiKey": ""
  },
  "Prefix": "/",
  "AutoRestart": true,
  "ReadMe": false,
  "Defalut": {
    "apiType": 1,
    "voiceType": "hikari"
  },
  "BlackLists": {
    "memberIds": [
      "381054450451742720"
    ],
    "prefixes": [
      "!",
      "/"
    ],
    "bots": true
  }
}
項目 内容・説明
discordToken Discordのトークンを記入
voiceTextApiKey VoiceTextのAPIキーを記入
Prefix コマンドの接頭語を決めます
AutoRestart 予期せぬエラー時に自動でボイスチャンネルへ再接続すかどうか
ReadMe このBotが送るメッセージを読み上げるかどうか
apiType デフォルトのAPIを指定 (利用できるAPIが1になってしまったので無意味)
voiceType デフォルトのボイスを指定
memberIds 読み上げから除外するユーザーのユーザーID
prefixes 読み上げから除外する接頭語
bots Botを読み上げから除外する

Botのコマンドについて

接頭語+α 内容・説明
join ボイスチャンネルにBotを呼びます
reconnect ボイスチャンネルへ再接続します
kill ボイスチャンネルから切断します
mode 読み上げに利用するTTSのAPIを変更します
type APIで利用可能な音声タイプを一覧表示します
voice 音声タイプを変更します
speed 音声の速度を変更します(0~200の数値)
pitch 音声の高さを変更します(0~200の数値)
reload コンフィグを再読み込みします

接頭語は、Configファイルで指定した接頭語を使います。

ToDo

一応まだ、開発のモチベーションがあるので今後の予定としては、ユーザーごとに読み上げ音声の種類を設定できるようにしたいと考えています。
聞き専の人が複数居ても、読み上げ音声の種類で人を区別できるようになるので利便性が上がると思います。

最後に

2018年2月頃に書いたプログラムをリファクタリングや機能の改善、バグ修正などしましたが、最終的には実用に足りる実装ができたと思います。
まだまだ、至らぬ点がありそうですが今後も開発出来たら良いなと思います。
最後まで見て頂きありがとうございます。

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

テキストエリアのカーソル位置に画像を挿入したい

はじめに

現在、社内向け(想定)の質問アプリケーションの開発を個人的に行っています。
開発詳細

こちらのアプリケーションでは、マークダウン記法を用いることができます。
Qiitaのように、非同期で複数画像を投稿できるよう実装をしたのですが現状テキストエリアの文末に画像を挿入するような仕様になっています。
しかし、実際に使ってみて文章の途中に画像を挿入したいシーンもあり使いにくさを感じました。

そこで今回は、テキストエリアのカーソル位置を取得しそこに画像を挿入するような処理を実装したので学習メモとして残します。

 流れ

流れとしては
1.対象の要素(今回はテキストエリア)の取得
2.テキストエリアの文字列をsubstring()でカーソル位置前後の2つの文字列に分割する(カーソル位置より前の文章とカーソル位置より後の文章)
3.あとは、上記の文字列の間に挿入したいもの(今回は画像URL)をいれる

実装

コードはこのようになりました。

let image = "![画像](" + response + ")";

let target = document.getElementById('markdown_editor_textarea');

let beforeCursor = target.value.substring(0, target.selectionStart);
let afterCursor = target.value.substring(target.selectionStart);
let newText = beforeCursor + image + afterCursor;

target.value = newText; 

ひとつひとつ解説。

let image = "![画像](" + response + ")";

こちらは、今回カーソル位置に挿入したい文字列になります。
今回、非同期処理で画像URLをサーバーから受け取っています。そのためこのresponseはAWS S3で保存した画像URLとなります。テキストエリアの横にプレビューがありそこで画像プレビューを表示させるためマークダウンの形式にしています。


let target = document.getElementById('markdown_editor_textarea');

getElementByIdで対象のテキストエリアの要素を取得。
ここで、getElementByIdでできるならgetElementsByClassNameでも出来るのかなと思いgetElementsByClassNameでやろうとしたらうまくいきませんでした(知識不足)。

てっきりgetElementsByClassNameはgetElementByIdのクラス版かと思っていましたが、返り値がそもそも違いました。

◯getElementById
対象要素が存在する場合はElementオブジェクトを返し、ない場合はnullを返す。
参考:特定のID名から要素を取得
※Elementオブジェクトとは、HTML及びXHTMLの要素を表現するオブジェクト

◯getElementsByClassName
対象要素が存在する場合は、1個以上のHTMLCollectionを返し、内場合は0個のHTMLCollectionを返す。
参考:特定のclass名(class属性値)から要素を取得

getElementsByClassNameは名前がElements複数形である通り、配列のように複数返す。
確かにclassはidと違い同一ページに同じ属性値を使用できるため複数返すようになっているのかなと。


let beforeCursor = target.value.substring(0, target.selectionStart);
let afterCursor = target.value.substring(target.selectionStart);

ここでは、テキストエリア内の文字列をカーソルの位置で分割している。
文字列の分割はsubstring()で行っている。
第一引数に分割の開始位置、第二引数に終了位置(指定しなければ文字列の最後まで)を指定できる。

※終了位置は含まれないという点に注意
例)

const str = "hoge";
console.log(str.substring(0,2)); //出力:ho

selectionStartでテキストエリアのカーソル位置を取得しています。
selectionStartは、選択範囲の開始位置のオフセット(つまりテキストエリアの開始位置からカーソルまでの距離)を返す。
これでカーソル位置の前後で分割が出来ます。


let newText = beforeCursor + image + afterCursor;
target.value = newText; 

分割した文字列の間に、挿入したい文字列をいれる。それをテキストエリアに上書きしてあげればOK。

まとめ

今回は、テキストエリアのカーソル位置に画像を挿入するにあたり学習したことをまとめました。
まだJavaScript部分では知識が圧倒的に足りていないので少しずつ理解を深めていきたいと思います。

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

【JS】初期値を設定したn個のArrayを生成する

fill を使うとよいです。
例えば、要素数12で全て0を設定したArrayを生成するには以下のようにします。

> Array(12).fill(0)
  => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/fill

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

JavaScriptで文字列のゼロパディング(C言語でいう%02d)

padStart を使うとよいです。
第一引数で何桁まで埋めるか指定し、第二引数で埋める文字を指定します。

> '1'.padStart(2, '0')
  => "01"

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/padStart

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

jestでテストを分類して個別に実行する

TL;DR

hoge.{test category}.test.tsのようにテストファイルを命名する
例. `hoge.unit.test.ts

実行時に対象ファイルを指定する

npx jest ".*\.unit\.test\.ts$"

jest.unit.config.jsのように設定ファイルを分割するのもアリ

動機

  • テストにはそれぞれ特性がある
  • 単体テストと言われるような、依存関係が少なく、高速なもの
  • 統合テストと呼ばれるような依存関係があるもの
  • DBや外部APIアクセスがあるもの
  • ビルドが必要なもの

これらを一括で実行するのは現実的ではない

対策

テストを適切に分類し、それぞれ任意のタイミングで呼び出す

案1. テストファイル名で分類する

hoge.unit.test.tsのようにファイル名にテスト分類を入れる規約を作る

  • pro: どのようなテストをするファイルかわかりやすい

案2. テスト分類ごとにディレクトリを分割する

└── __tests__
    ├── integration
    │   └─── hoge.test.ts
    └─── unit
         └─── hoge.test.ts
  • pro: testコードをプロダクトコードと分けている場合はわかりやすい
  • con: testコードをプロダクトコードと並列にしている場合は使えない

案3. テストケース名に分類を入れる

試していないが以下の方な方法もできる?

describe("unit: hogeのテスト",() => {
  expect(hoge).toBe(true);
}

備考

テストをどう分類するかについてはまた別の課題とする
動機を考えると、test sizesでの分類が適しているかもしれない

結合テストと呼ぶのをやめた話

より良い手法があればコメントください

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

js2,クリックしたら動作する。初心者 初学者

②クリックしたら○○する
今回はJS(JavaScript)でとても良く使う、イベントの処理について解説しています。
HTMLからタグを取得して、それにイベントを紐付けて何か処理をさせてあげるところまで紹介しています。
クリックしたら、文字列を書き換えるところまで実施してるので、是非活用してみてください!

document.getElementById("test").addEventListener("click", function() {

})

.addEventListener

Add
は追加する

EventをListener
イベントを監視役

("click", function() {

})
クリックすると次{指令}を動作する

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

【JavaScript】ラッパーオブジェクトについて

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

JavaScript におけるデータは「すべてオブジェクトである」

と、いうふうに聞いたことがある。

しかし、学習を進める上で

「データ型のうちObjectはオブジェクト型、それ以外をプリミティブ型という。」とのことで、矛盾していないか?との思いがよぎった。

詳しくはこちら
【JavaScript】JavaScriptにおける変数の参照について

その答えが、「ラッパーオブジェクト」であった。

例えば、以下のような式があるとする。

let str = "name";
let big = str.toUpperCase();

console.log(big);

//NAME

toUpperCase()関数は、呼び出し元の文字列を大文字にして返却する関数であるが、なぜプリミティブ型のであるstring型のstrが関数を呼べるのか。
実際、下記の様にデータ型を調べてみると、

console.log(typeof str);
//string

string型である。

この理由はJavaScriptの「ラッパーオブジェクトの自動変換」という機能にある。

ラッパーオブジェクトというのは、本来オブジェクト型でない値を、オブジェクトで包んで(ラップして)オブジェクトとしての振る舞いを持たせるというものである。

つまり、

str.toUpperCase();

この様に、string型のデーターに対し、オブジェクトとしての振る舞いを要求された時は、ラッパーオブジェクトで自動的に包んで、実行されるのである。

str.toUpperCase();

この時

(new String(str)).toUpperCase();

この様に自動的に、string型に対応するオブジェクトであるStringからインスタンスが作成され、関数を呼び出している。(Stringオブジェクトは、toUpperCaseメソッドを保有している。)

実際、開発者はこの自動変換を意識せずともコードは書くことができるので、string型もオブジエクとして扱うことができる。

つまり

JavaScript におけるデータは「すべてオブジェクトである」のではなく、ラッパーオブジェクトのおかげで、「全てのデーターはオブジェクトのように振舞うことができる」と言うことなんですね。

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