20210110のJavaScriptに関する記事は18件です。

LINEボットでGoogle Alertを通知

Google Alertは、キーワードを登録しておくと、ウェブ上の新着コンテンツを知らせてくれるサービスです。私は、今まで、メールで受信していたのですが、せっかくコンテンツを通知してくれたのに、時間がたつと埋もれてしまっていました。

そこで、新着コンテンツが来たタイミングでLINEボットで通知すると同時に、データベースに登録して、あとでも参照できるようにしてみます。

image.png

2か所でNode.jsを使います。
1つ目は、コンテンツの定期的な取得のため。
2つ目は、過去コンテンツの格納とLINEボット用のサーバです。

過去コンテンツは、MySQLサーバに格納しています。

もろもろはGitHubに上げておきました。

poruruba/GoogleAlert
 https://github.com/poruruba/GoogleAlert

流れ

①Googleアラートに、キーワードを登録しておきます。そうすると、RSSフィードのURLが取得できます。
②Node.jsなどで、定期的にRSSフィードからコンテンツを取得するとともに、Node.jsサーバにコンテンツ登録を依頼します。
③Node.jsサーバでは、コンテンツがすでにデータベースに登録されているか確認し、登録されていない場合はデータベースに登録します。それと同時に、LINEボットにコンテンツをメッセージとして送信します。
④ユーザは、LINEアプリにコンテンツがメッセージで送信されてきます。
⑤(必要に応じて)ユーザはLIFFアプリを起動し、Node.jsサーバからコンテンツ一覧を取得し表示します。

準備:Googleアラートにキーワードを登録

以下のサイトでキーワードを登録します。

Googleアラート
 https://www.google.co.jp/alerts

image.png

アラートを作成、と表示されているところにキーワードを入力します。
今回は、「ESP32」としてみました。オプションを表示となっている場合はクリックしてオプションを表示します。

image.png

ここで、配信先として、自身のGmailアドレスではなく、「RSSフィード」を選択します。
最後に、アラートを作成 を押下します。

そうすると、ESP32が追加され、無線のようなマークがでていますので、クリックします。
そうすると、RSSフィードが表示されました。
まだコンテンツの監視が始まったばかりで、コンテンツは1件もないです。ブラウザに表示されているこのURLを覚えておきます。

データベースの準備

以下のようなスキーマのテーブルを作成しました。

データベース名:googlealert

テーブル名:items
コンテンツを格納します。

image.png

テーブル名:members
LINEボットからコンテンツをメッセージ送信する先のユーザIDを格納します。

image.png

LINEボットの作成

すみませんが、以下の投稿を参考にしてください。

 LINEボットを立ち上げるまで。LINEビーコンも。

LINEボット名は「Googleアラート」にしてみました。

定期的なコンテンツの取得

定期的なコンテンツ取得は、GoogleアラートのRSSフィードを参照することで行います。
また、これから立ち上げるNode.jsサーバへコンテンツをHTTP Postしています。

RSSフィードの参照およびHTTP Postには以下のnpmモジュールを使っています。

rbren/rss-parser
 https://github.com/rbren/rss-parser

node-fetch/node-fetch
 https://github.com/node-fetch/node-fetch

cron_googlealert/index1.js
'use strict';

const GOOGLE_ALERT_RSS_URL = process.env.GOOGLE_ALERT_RSS_URL || '【GoogleアラートのRSSフィードのURL】';
const GOOGLE_ALERT_SEARCH_KEYWORD = process.env.GOOGLE_ALERT_SEARCH_KEYWORD || '【Googleアラートに指定したキーワード】';

const base_url = "【Node.jsサーバのURL】";

const fetch = require('node-fetch');
const { URL, URLSearchParams } = require('url');
const Headers = fetch.Headers;

const Parser = require('rss-parser');
const parser = new Parser();

(async () =>{
  var feed = await parser.parseURL(GOOGLE_ALERT_RSS_URL);  
  if( feed.items.length <= 0 )
    return;

  feed.items.forEach(item =>{
    console.log(item.title);
  });

  try{
    var created_at = new Date().getTime();
    for( var i = 0 ; i < feed.items.length ; i++ ){
      var item = feed.items[i];
      console.log(item);
      var param = {
        keyword: GOOGLE_ALERT_SEARCH_KEYWORD,
        title: item.title,
        pubDate: item.pubDate,
        contentSnippet: item.contentSnippet,
        id: item.id,
        link: item.link,
        created_at: created_at
      };
      await do_post(base_url + '/linebot-googlealert-push', param );
    }
  }catch(error){
    console.error(error);
  }
})();

function do_post(url, body) {
  const headers = new Headers({ "Content-Type": "application/json; charset=utf-8" });

  return fetch(new URL(url).toString(), {
      method: 'POST',
      body: JSON.stringify(body),
      headers: headers
    })
    .then((response) => {
      if (!response.ok)
        throw 'status is not 200';
      return response.json();
    });
}

以下の部分を、各自の環境に合わせて変更します。これから立ち上げるNode.jsサーバのURLです。

【Node.jsサーバのURL】

Node.jsサーバの実装

とりあえず、以下ダウンロードしてNode.jsサーバを立ち上げます。

unzip GoogleAlert-master.zip
cd GoogleAlert-master
mkdir cert
npm install

HTTPSである必要がありまして、SSL証明書をcertフォルダに置きます。フォルダ名は、app.jsを見ればわかります。
起動は以下の通りです。

$ node app.js

RSSフィードされたコンテンツを受信する部分を抜粋します。

server/api/controllers/linebot-googlealert/index.js
exports.handler = async (event, context, callback) => {
  if( event.path == '/linebot-googlealert-push' ){
    var body = JSON.parse(event.body);

    var sql_query = `SELECT id FROM items WHERE id = '${body.id}'`;
    const [rows] = await dbconn.query(sql_query);

    var index = rows.findIndex(rows_item => body.id == rows_item.id );
    if( index < 0 ){
      var sql_insert = `INSERT INTO items (id, keyword, content, pubDate, created_at) VALUES ('${body.id}', '${body.keyword}', '${JSON.stringify(body)}', '${new Date(body.pubDate).getTime()}', ${body.created_at})`;
      await dbconn.query(sql_insert);

      var sql_select = `SELECT memberId FROM members`;
      const [rows] = await dbconn.query(sql_select);

      var message = app.createSimpleCard(body.title, 'キーワード: ' + body.keyword, body.contentSnippet, 'ブラウザで開く', { type: 'uri', uri: body.link } );
      rows.forEach( row =>{
        app.client.pushMessage(row.memberId, message);
      });
    }

    return new Response({});
  }else

以下の部分を環境に合わせて変更します。

server/api/controllers/linebot-googlealert/index.js
const DB_HOST = '【MySQLサーバのホスト名】';
const DB_USER = '【MySQLサーバのユーザ名】';
const DB_PASSWORD = "【MySQLサーバのパスワード】";
const DB_PORT = 3306;
const DB_DATABASE = "googlealert";

上記のうち、以下の部分がLINEボットとしてメッセージ送信する部分です。

server/api/controllers/linebot-googlealert/index.js
      var message = app.createSimpleCard(body.title, 'キーワード: ' + body.keyword, body.contentSnippet, 'ブラウザで開く', { type: 'uri', uri: body.link } );
      rows.forEach( row =>{
        app.client.pushMessage(row.memberId, message);
      });

以下の部分を環境に合わせて変更します。

server/api/controllers/linebot-googlealert/index.js
const config = {
  channelAccessToken: '【LINEボットのチャネルアクセストークン(長期)】',
  channelSecret: '【LINEボットのチャネルシークレット】',
};

上記のシークレットを変更しないと、LINEボットのWebhook設定で、Webhook URLの検証が成功しないです。

コンテンツ取得とLINE通知を試してみる。

それでは、LINEボットを自身のスマホのLINEアプリから登録しましょう。
登録が完了すると、LINEボットがそれを認識し、LINEユーザのユーザIDをデータベースに登録します。

image.png

以下の部分です。

server/api/controllers/linebot-googlealert/index.js
app.follow(async (event, client) =>{
  var memberId = (event.source.type == 'user') ? event.source.userId : event.source.groupId;
  var sql_insert = `INSERT INTO members (memberId, type) VALUES ('${memberId}', '${event.source.type}')`;
  await dbconn.query(sql_insert);
});

app.unfollow(async (event, client) =>{
  var memberId = event.source.type == 'user' ? event.source.userId : event.source.groupId;
  var sql_delete = `DELETE FROM members WHERE memberId = '${memberId}' AND type = '${event.source.type}'`;
  await dbconn.query(sql_delete);
});

exports.fulfillment = app.lambda();

そして、定期的なコンテンツ取得として用意したcron_googlealert/index1.jsを起動します。

起動に便利な、シェルスクリプトを用意しました。

cron_googlealert/index1.sh
#!/bin/sh

export GOOGLE_ALERT_RSS_URL="【GoogleアラートのRSSフィードのURL】"
export GOOGLE_ALERT_SEARCH_KEYWORD=" 【Googleアラートに指定したキーワード】"
cd /home/XXXX/projects/node/cron_googlealert
/home/XXXX/.nvm/versions/node/v12.19.0/bin/node index.js

環境に合わせて以下を変更します。後者は、「ESP32」でした。

【Googleアラートに指定したキーワード】
【GoogleアラートのRSSフィードのURL】

$cd cron_googlealert
$chmod +x index1.sh
$ ./index1.sh

(まだコンテンツは見つかっていないかもしれません。気長に待ちましょう)

別のキーワードですが以下のようにDBに登録され、LINEにも通知されます。

image.png

同時に、LINEアプリにも通知が届いているかと思います。

image.png

あとは、これをCronで起動すればよいです。例えば、1時間ごとに。

$crontab -e
★以下を入力★
15 * * * * /home/XXXX/projects/node/cron_googlealert/index1.sh

コンテンツ一覧表示するLIFFアプリ

普通のWebページでもよいのですが、せっかくなのでLIFFアプリにして、LINEアプリ内で表示できるようにします。

LIFFアプリの登録には、LINE Developersで作ったMessaging APIのチャネルではなく、LINEログインのチャネルが必要です。

LINE Developers
https://developers.line.biz/console/

登録が完了すると、LIFF IDが割り当たります。

Node.jsサーバの以下の部分を書き換えます。

server/api/controllers/linebot-googlealert/index.js
const LIFF_ID = "【LINEのLIFF-ID】";

image.png

画面はこんな感じです。

image.png

HTMLはこんな感じです。

public/googlealert/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <link rel="stylesheet" href="css/start.css">
  <script src="js/methods_bootstrap.js"></script>
  <script src="js/components_bootstrap.js"></script>
  <script src="js/vue_utils.js"></script>

  <script src="dist/js/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>

  <title>Google Alert</title>
</head>
<body>
    <div id="top" class="container">
        <button class="btn btn-default pull-right" v-on:click="list_update">更新</button>
        <h1>Google Alert</h1>
        <br>

        <h2>本日のアイテム</h2>
        <div class="panel panel-default" v-for="(value, index) in item_list_today">
          <div class="panel-heading"><h3>{{value.content.title}}</h3></div>
          <div class="panel-body">
            <span class="pull-left">pubDate: {{new Date(value.pubDate).toLocaleString()}}</span>
            <span class="pull-right">keyword: {{value.keyword}}</span>
            <br><br>
            {{value.content.contentSnippet}}
          </div>
          <div class="panel-footer text-right">
            <a class="pull-left" v-bind:href="value.content.link">ブラウザで開く</a>
            いいね数:{{value.likes}}
            <button class="btn btn-default btn-sm" v-on:click="change_likes(value, true)"></button><button class="btn btn-default btn-sm" v-on:click="change_likes(value, false)"></button>
          </div>
        </div>

        <hr>
        <h2>過去のアイテム</h2>
        <div class="form-inline">
          <button class="btn btn-default" v-on:click="list_update_default">今月</button>
          <select class="form-control" v-model.number="target_year" v-on:change="list_update">
            <option v-for="(value, index) in target_year_list" v-bind:value="value">{{value}}年</option>
          </select>
          <select class="form-control" v-model.number="target_month" v-on:change="list_update">
            <option value="0">通年</option>
            <option v-for="(value, index) in [1,2,3,4,5,6,7,8,9,10,11,12]" v-bind:value="value">{{value}}月</option>
          </select>
          <select class="form-control" v-model.number="has_likes">
            <option value="1">いいね有のみ</option>
            <option value="0">すべて</option>
          </select>
        </div>

        <table class="table table-striped">
          <thead>
            <tr><th>keyworkd</th><th>title</th><th>pubDate</th><th>いいね</th></tr>
          </thead>
          <tbody>
              <tr v-for="(value, index) in item_list" v-if="has_likes==0||value.likes>0">
                  <td>{{value.keyword}}</td>
                  <td><a v-bind:href="value.content.link">{{value.content.title}}</a></td>
                  <td>{{new Date(value.pubDate).toLocaleString()}}</td>
                  <td>{{value.likes}}
                    <button class="btn btn-default btn-xs" v-on:click="change_likes(value, true)"></button><button class="btn btn-default btn-xs" v-on:click="change_likes(value, false)"></button>
                  </td>
                </tr>
          </tbody>
        </table>


        <!-- for progress-dialog -->
        <progress-dialog v-bind:title="progress_title"></progress-dialog>
    </div>

    <script src="js/start.js"></script>
</body>

Javascriptはこんな感じです。

public/googlealert/js/start.js
'use strict';

//var vConsole = new VConsole();

const base_url = "【Node.jsサーバのURL】";


var vue_options = {
    el: "#top",
    data: {
        progress_title: '', // for progress-dialog

        item_list_today: [],
        item_list: [],
        target_month: 0,
        target_year: 0,
        target_year_list: [],
        has_likes: 0
    },
    computed: {
    },
    methods: {
        list_update_default: async function(){
            this.target_month = this.now.getMonth() + 1;
            this.target_year = this.now.getFullYear();
            return this.list_update();
        },
        list_update_today: async function(today){
            var param = {};
            var list = await do_post(base_url + "/linebot-googlealert-list", param );
            for( var i = 0 ; i < list.length ; i++ )
                list[i].content = JSON.parse(list[i].content);
            this.item_list_today = list;
        },
        list_update: async function(){
            var param = {
                year: this.target_year,
                month: this.target_month,
            };
            var list = await do_post(base_url + "/linebot-googlealert-list", param );
            for( var i = 0 ; i < list.length ; i++ )
                list[i].content = JSON.parse(list[i].content);
            this.item_list = list;
        },
        change_likes: async function(target, increment){
            console.log(target);
            var target_likes = ( increment ) ? (target.likes + 1) : (target.likes - 1);
            if( target_likes < 0 ) target_likes = 0;
            var param = {
                id: target.id,
                likes: target_likes
            };
            await do_post(base_url + "/linebot-googlealert-likes", param );
            var t1 = this.item_list.find(item => item.id == target.id );
            if( t1 )
                this.$set(t1, "likes", target_likes);
            var t2 = this.item_list_today.find(item => item.id == target.id );
            if( t2 )
                this.$set(t2, "likes", target_likes);
        },
    },
    created: function(){
    },
    mounted: async function(){
        proc_load();

        this.now = new Date();
        for( var i = 0 ; i < 5 ; i++ )
            this.target_year_list.push(this.now.getFullYear() - i );
        this.target_month = this.now.getMonth() + 1;
        this.target_year = this.now.getFullYear();

        this.list_update_today();
        this.list_update();
    }
};
vue_add_methods(vue_options, methods_bootstrap);
vue_add_components(vue_options, components_bootstrap);
var vue = new Vue( vue_options );

function do_post(url, body) {
    const headers = new Headers({ "Content-Type": "application/json; charset=utf-8" });

    return fetch(new URL(url).toString(), {
        method: 'POST',
        body: JSON.stringify(body),
        headers: headers
      })
      .then((response) => {
        if (!response.ok)
          throw 'status is not 200';
        return response.json();
      });
  }

Node.jsサーバ側では、それにこたえられるように、以下のエンドポイントを用意しています。
一覧の取得といいねカウントです。一覧の取得では、本日のコンテンツ、月ごとのコンテンツ、年ごとのコンテンツ、のようにフィルタリングして返しています。

server/api/controllers/linebot-googlealert/index.js
  if( event.path == '/linebot-googlealert-list' ){
    var body = JSON.parse(event.body);

    var startTime;
    var endTime;
    if( !body.year || !body.month ){
      var today = new Date();
      today.setHours(0, 0, 0, 0);
      startTime = today.getTime();
      var tomorrow = new Date(today);
      tomorrow.setDate(today.getDate() + 1);
      endTime = tomorrow.getTime();
    }else
    if( body.year && body.month == 0 ){
      var thisYear = new Date();
      thisMonth.setFullYear(body.year);
      thisMonth.setMonth(0);
      thisMonth.setDate(1);
      thisMonth.setHours(0, 0, 0, 0);
      startTime = thisYear.getTime();
      var nextYear = new Date(thisYear);
      nextMonth.setFullYear(thisYear.getFullYear() + 1);
      endTime = nextYear.getTime();
    }else{
      var thisMonth = new Date();
      thisMonth.setFullYear(body.year);
      thisMonth.setMonth(body.month - 1);
      thisMonth.setDate(1);
      thisMonth.setHours(0, 0, 0, 0);
      startTime = thisMonth.getTime();
      var nextMonth = new Date(thisMonth);
      nextMonth.setMonth(thisMonth.getMonth() + 1);
      endTime = nextMonth.getTime();
    }

    var sql_select = `SELECT * FROM items WHERE pubDate >= ${startTime} AND pubDate < ${endTime} ORDER BY pubDate DESC`;
    const [rows] = await dbconn.query(sql_select);

    return new Response(rows);
  }else
  if( event.path == '/linebot-googlealert-likes' ){
    var body = JSON.parse(event.body);

    var sql_update = `UPDATE items SET likes = ${body.likes} WHERE id = '${body.id}'`;
    await dbconn.query(sql_update);

    return new Response({});
  }

以上

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

NLP(日本語)で使えるJSのDateライブラリを公開しました

はじめに

NLPでDateを操作できるJavaScript向けのOSSを作って、githubとCDNに公開しました。

「…NLPって何?」と思われたかと思いますが、
「自然言語解析(Natural Language Processing)」のことです。

区別 言語
機械が理解できる言語 マシン語(0と1の羅列)
人間が読み書きできて、機械も理解できる言語 プログラミング言語
人間が普段しゃべっている言葉 自然言語

機械の言葉は人間には理解できず、人間の言葉も機械には理解できません。
だから、人間と機械の共通言語としてプログラミング言語ができました。

ですが、今は機械が人間の言葉を直接理解する場面が身近にあります。
たとえば、アレクサ、Siri、翻訳サイト、Google検索だってそうです。

【スマートスピーカーの場合】
人間が言葉で話しかける → 機械が理解 → 何か処理をする

この「人間の言葉を理解する仕組み」が自然言語解析と呼ばれています。

NLPでJavaScriptの日時を操作する

アレクサに話しかける感じで、自然言語でプログラミングがしたい。
そう思って、JavaScript向けのライブラリを作って公開しました。

これまで(JavaScript標準)の日時操作

これまでの「プログラミング言語」の日時操作はこうでした。

let date = new Date();
date.setDate(date.getDate() + 1); // 明日の
date.setHours(12 + 3); // 午後3時
date.setMinutes(12); // 12分
date.setSeconds(10); // 10秒

setDateは日を設定、一文字違いのsetDayは曜日、setMonthだけは0から始まる、みたいなルールがありました。
文字列にするなら、そこからさらにフォーマッターで変換して…みたいなことをしていました。
ソースは長く、学習コストがかかり、可読性もいまいちです。

NLPでの日時操作

自然言語ではこう書きます。

// textには"令和3年01月11日 03時12分10秒"が入ります
let text = NLPDate("明日の午前3時12分10秒").asString("和暦の年月日時分秒");

日本語で書いた文章が、そのままJavascriptで使えるデータになります。
ソースは短く、日本語さえできれば学習コストはゼロ、可読性も高くなります。

NLPDateの使い方

現在日時を取るには、「現在」を渡して、asDateで取得結果をDate型にキャストします。

// new Date()と同じ現在日時が取れます
let date = NLPDate("現在").asDate();

3時間後の現在時間を取るには、「現在から3時間後」と書くだけです。

// new Date()から3時間後の日時が取れます
let date = NLPDate("現在から3時間後").asDate();

「次の火曜日の午後3時20分」と書けば、今日は月曜ですから、明日の午後3時20分が取れます。

// Mon Jan 11 2021 08:15:52 GMT+0900 (日本標準時)の時に実行すると、
// Tue Jan 12 2021 15:20:00 GMT+0900 (日本標準時)の時間になります。
let date = NLPDate("次の火曜日の午後3時20分").asDate();

「現在から3時間後の四日後の20秒後、それから五年後」などと操作を続けることもできます。もちろん現在以外の日時を操作したり、同じ単位への操作を続けて、「明日の今頃から3時間後の3時間後の4時間後の7時間後」などと書くこともできます。

// Mon Jan 11 2021 08:10:23 GMT+0900 (日本標準時)の時に実行すると、
// Thu Jan 15 2026 11:10:43 GMT+0900 (日本標準時)の時間になります。
let date = NLPDate("現在から3時間後の四日後の20秒後、それから五年後").asDate();

// Mon Jan 11 2021 08:08:33 GMT+0900 (日本標準時)の時に実行すると、
// Wed Jan 13 2021 01:08:33 GMT+0900 (日本標準時)の時間になります
let date = NLPDate("明日の今頃から3時間後の3時間後の4時間後の7時間後").asDate();

Date型ではなく文字列でほしいのなら、asStringでキャストします。
ほしいフォーマットを自然な日本語で書けば、そのフォーマットになります。

// 日本語のフォーマットの文字列になります
// textには、"2021年07月30日 08時26分42秒"が入ります
let text = NLPDate("2021年7月30日の今頃").asString("日本語の年月日時分秒");

何も指定しなければISO8601形式になります。

// ISO8601のフォーマットの文字列になります
// textには、"2021-07-30T00:00:00+09:00"が入ります
let text = NLPDate("2021年7月30日").asString();

ほかの日時と比較したいのなら、asNumberで数値にキャストするのが便利です。

let date = NLPDate("現在");
if (NLPDate("2020年1月").asNumber("年月") <= date.asNumber("年月") &&
    date.asNumber("年月") < NLPDate("2020年8月").asNumber("年月")) {
    // 2020年8月のasNumber("年月")は202008になります。
    // 数値でとるので、if文で大小比較ができます
    console.log("今日は2020年1月~2020年7月末までの間です");
}

あいまいな表現

「2021年2月の第三水曜日の午後3時」のように、
漢数字、半角数字、全角数字の表記ゆれがあっても正常に動作します。

// date には"2021-02-17T15:00:00+09:00"のDateオブジェクトが入ります
let date = NLPDate("2021年2月の第三水曜日の午後3時").asDate();

うろ覚えになっても、名前が半分あっていれば正常に判定します。

// "ISO8601"がうろ覚えですが、正常に解析します。
// date には"2021-07-31T00:00:00+09:00"の文字列が入ります。
let date = NLPDate("2021年7月30日の月末").asString("INO8688");

一部に日時が入っていれば、日時だけを読み取ります。

// date には"2021-01-12T00:00:00+09:00"のDateオブジェクトが入ります
let date = NLPDate("ふるさと納税 ワンストップ特例申請に関する申請期限(令和3年1月12日)などを掲載しました。").asDate();

最後のキスは煙草のフレーバーがしました、泣いているのはいつでしょう。

// date には"2021-01-11T20:56:24+09:00"のDateオブジェクトが入ります
let date = NLPDate("明日の今頃には私はきっと泣いている").asDate();

たぶん君が来るのは2分後です。

// date には"2021-01-10T02:00:00+09:00"のDateオブジェクトが入ります
let date = NLPDate("午前2時、踏切に、望遠鏡を担いでった").asDate();

人間の言葉で書けると、ぐっと簡単になる感じがしないでしょうか。

使い方はシンプルで、覚えるメソッドは3つだけです。自然言語で日時を作ったあと、その結果が文字列型でほしいのなら「asString」、数値でほしいのなら「asNumber」、Date型でほしいのなら「asDate」を使って変換します。

導入して使ってみる

NLPDateは、ブラウザ、node、コマンドプロンプト、bashで使えます。
ライブラリのサイズは40KBほどで、ローカルに保存すればオフラインで使えます。

ソースコード

NLPDateのレポジトリはこちらにあります。
https://github.com/ShotaOki/NLPDate

実際にブラウザで動かしているサンプルがこちらにあります。
https://shotaoki.github.io/NLPDate.github.io/

ブラウザに導入する

CDNで読み込ませることができます。
以下のテキストを HTML のヘッダに置いてください。

<script src="https://cdn.jsdelivr.net/gh/ShotaOki/NLPDate@v0.0.1/modern/nlpdate-main.min.js"></script>

※もし IE11 で動かす必要があれば、上のリンクの代わりにこちらを置いてください。

<!-- ES2015版:新しいブラウザのほか、IE11でも動きます。通常版よりもファイルサイズが大きくなります -->
<script src="https://cdn.jsdelivr.net/gh/ShotaOki/NLPDate@v0.0.1/es2015/nlpdate-main.min.js"></script>

コマンドプロンプトやbashに導入する

githubからプロジェクトをCloneしたあと、「${Cloneしたフォルダ}/bin」にパスを通します。
コマンドプロンプトやbashから使えるようになります。

nlp-date "平成元年1月15日の今頃" -f "スラッシュ区切りの年月日時分秒"
>> 1989/01/15 21:34:29

おわりに

まだしばらくバグ取りや安定化の時間が必要だと思います。

自然言語でプログラミングができたら楽だ、ソースも読みやすくなる、と思ってくださる方がおられましたら、こちらのライブラリを覚えておいていただくか、フィードバックや参加をいただけないかと思っています。

ぜひ今後ともよろしくお願いいたします。

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

【JavaScript】innerHTMLの使い方

プログラミング勉強日記

2021年1月10日
今日はinnerHTMLの使い方について簡単にまとめる。

innerHTMLとは

 HTML要素の中身を変更するときに使われるプロパティである。HTML要素の中身を自由に変更することで、動的なWebページを作成できる。
 

innerHTMLの書き方

 innerHTMLプロパティは、読み込みと書き込みの両方に対応している。

innerHTMLの書き方
指定する要素名.innerHTML;

// 変数に指定要素のHTMLコードを代入する
let sample = 指定する要素名.innnerHTML;

// 指定要素に変数の値を代入
指定する要素名.innnerHTML = sample2;

HTML要素を取得する方法

サンプルコード
<html>
  <body>

    <div id = "idName">
      <p>Hello World</p>
    </div>

  <script>
    var mydiv = document.getElementById("idName");

    console.log(mydiv.innerHTML);
  </script>

  </body>
</html>
コンソール結果
Hello World

HTML要素に値を追加する方法

サンプルコード
<html>
  <body>

    <h1 id="idName">クリック前</h1>
    <input type="button" value="Click" onclick="myfunc()">

    <script>
      var myfunc = function(){

        var myh1 = document.getElementById("idName");
        myh1.innerHTML = "クリック後";


      }
    </script>

  </body>
</html>

実行結果
image.pngimage.png

HTMLタグを変更する方法

サンプルコード
<html>
  <body>

    <div id="idName">
      <h1>クリック前</h1>
    </div>

    <input type="button" value="Click" onclick="myfunc()">

    <script>
      var myfunc = function(){

        var mydiv = document.getElementById("idName");
        mydiv.innerHTML = "<h3>クリック後(h3に変更)</h3>";

      }
    </script>

  </body>
</html>

実行結果
image.pngimage.png

参考文献

要素の中身を変える!JavaScriptでinnerHTMLの使い方【初心者向け】
【JavaScript入門】innerHTMLでdivタグ内の要素を取得、設定する方法

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

NeuralnetworkをJavaScriptで実装した

はじめに

色々な機械学習処理をブラウザ上で試せるサイトを作った」中で実装したモデルの解説の十三回目です。

今回はNeuralnetworkの実装について解説します。
MLP, Autoencoder, GAN, VAE, DQN/DDQNで使っています。

デモはこちらから。
実際のコードはneuralnetwork.jsにあります。

なお、可視化部分や個別のモデルについては一切触れません。

概説

ニューラルネットワークそのものについては調べればすぐに情報が手に入ると思いますので、詳細は割愛します。

また、私は次のような知識の下で作成していますので、参考になるかどうかわかりません。ご注意ください。

  • TensorflowやKerasやConvNetJSやNeuralNetworkConsoleといったものの使用感は知っているものの、その内部実装は詳しくない
  • Define by runとかDefine and runとか言葉は知っているが、具体的にどんな利点があるのか理解しておらず、どう違うのかもよく分からない
  • ニューラルネットワークの数学的知識は多少ある

Neuralnetworkの全体のコードはここ、Worker用のコードはここにあります。
また、行列及びその演算はここにあります。

なお、現在は三次元以上のデータに対応していないため、二次元以上のCNNや、RNNは実装することが難しいです。そのうちに対応するかもしれません。

計算グラフ

Neuralnetworkは各層をノードとする計算グラフ、つまり有向グラフに帰着できるので、有向グラフ構造を基本として実装します。

有向グラフの実装方法はいくつかありますが、今回はシンプルに各ノードにその親ノードを持つように実装しました。
始点となるノードから順番に処理していったときに、いろいろとやりやすいですし。

※説明にはグラフ理論から「グラフ」「ノード」と呼んでいますが、実装上は機械学習から「ネットワーク(network)」「層(layer)」を使っています。

I/F

インタフェースはConvNetJSを参考に、オブジェクトの配列を受け取るようにしました。
各オブジェクトがノードにあたり、それぞれに自分の名前と、親ノードの名前の配列を持つことで、グラフ構造を構築できるようにしました。

また、使いやすくしたり、Conditional GANやVAEやDDQNを扱えるように、

  • 親ノードが未指定の場合は直前に定義されたノードのみを親ノードとする
  • 出力の指定がない場合は、最後のオブジェクトを出力とする
  • 複数の入力を持つことができる
  • 途中のノードの出力を複数取得できる
  • 定数を使用する場合は、数値をそのまま指定できる
  • データを分割した後の一部の取得は、[]によるアクセスを文字列として受け取る
  • 損失関数は文字列で指定することも、特定のオブジェクトの出力を指定することもできる
  • 別のグラフを間に挟むことができる
  • 複製できる

などとしています。

例えば、三層のMLPは次のような感じに定義できるようにします。

[
  { type: 'input' },
  { type: 'full', out_size: 10 },
  { type: 'sigmoid' },
  { type: 'full', out_size: 1 }
]

また、Conditional GANは次のように定義できるようにします。

discriminator
[
  { type: 'input', name: 'dic_in' },
  { type: 'input', name: 'cond', input: [] },
  { type: 'onehot', name: 'cond_oh', input: ['cond'] },
  { type: 'concat', input: ['dic_in', 'cond_oh'] },
  { type: 'full', out_size: 10, activation: 'tanh' },
  { type: 'full', out_size: 10, activation: 'tanh' },
  { type: 'full', out_size: 2 },
  { type: 'softmax' }
]
generator
[
  { type: 'input', name: 'gen_in' },
  { type: 'input', name: 'cond', input: [] },
  { type: 'onehot', name: 'cond_oh', input: ['cond'] },
  { type: 'concat', input: ['gen_in', 'cond_oh'] },
  { type: 'full', out_size: 10, activation: 'tanh' },
  { type: 'full', out_size: 10, activation: 'tanh' },
  { type: 'full', out_size: 2 },
  { type: 'leaky_relu', a: 0.1, name: 'generate' },
  { type: 'include', id: discriminatorId, input_to: 'dic_in', train: false }
]

discriminatorIdは先に定義したdiscriminatorを一意に特定できる値です

グラフ

まずグラフ全体をいい感じに管理するクラスを作成します。

コンストラクタでは、学習(fit)、順伝播(calc)、逆伝播(grad)、パラメータ更新(update)、複製(copy)のそれぞれの処理をやりやすいよう、次の処理を行います。

  • 出力が存在しない場合は追加
  • 損失関数がコンストラクタで渡された場合はノード化
  • 定数のノード化
  • 各ノードの親ノード情報の取得

順伝播、逆伝播では、始めに全ノードに対して必要な変数の束縛を行います。そうしてから、どのノードに何を渡すのかを適宜確認しながら、それぞれのノードの処理を実行しています。
今回は親の情報を持っているので、順伝播では自分の「位置」に結果を格納して使用する側でそれらを受け取り、逆伝播では逆に自分を使用するノードの「位置」に渡します。

学習は順伝播、逆伝播、更新の処理を順番に実施するだけです。

コード全体は以下の通りです。

class NeuralNetwork {
    constructor(layers, loss) {
        this._request_layer = layers;
        this._layers = [];
        if (layers.filter(l => l.type === 'output').length === 0) {
            layers.push({type: 'output'})
        }
        if (loss) {
            layers.push({type: loss})
        }
        const const_numbers = new Set();
        for (const l of layers) {
            if (l.input && Array.isArray(l.input)) {
                for (let i = 0; i < l.input.length; i++) {
                    if (typeof l.input[i] === 'number') {
                        const_numbers.add(l.input[i]);
                        l.input[i] = `__const_number_${l.input[i]}`;
                    }
                }
            }
        }
        if (const_numbers.size) {
            layers[0].input = [];
        }
        for (const cn of const_numbers) {
            const cl = new NeuralnetworkLayers.const({value: cn, size: 1, input: []})
            cl.network = this;
            cl.name = `__const_number_${cn}`
            cl.parent = [];
            this._layers.push(cl);
        }
        for (const l of layers) {
            const cl = new NeuralnetworkLayers[l.type](l);
            cl.network = this;
            cl.name = l.name;
            cl.parent = [];
            cl.input = l.input;
            if (l.input) {
                if (typeof l.input === 'string') {
                    l.input = [l.input];
                }
                for (const i of l.input) {
                    const subscriptRegexp = /\[([0-9]+)\]$/;
                    const m = i && i.match(subscriptRegexp);
                    const subscript = m ? +m[1] : null;
                    const name = m ? i.slice(0, -m[0].length) : i;
                    const tl = this._layers.filter(l => name === l.name);
                    cl.parent.push({
                        layer: tl[0],
                        index: this._layers.indexOf(tl[0]),
                        subscript: subscript
                    });
                }
            } else {
                const pid = this._layers.length - 1;
                if (pid >= 0) {
                    cl.parent.push({
                        layer: this._layers[pid],
                        index: pid,
                        subscript: null
                    });
                }
            }
            this._layers.push(cl);
        }
    }

    copy() {
        const cp = new NeuralNetwork(this._request_layer);
        for (let i = 0; i < this._layers.length; i++) {
            cp._layers[i].set_params(this._layers[i].get_params());
        }
        return cp;
    }

    calc(x, t, out, options = {}) {
        let data_size = 0
        if (Array.isArray(x)) {
            x = Matrix.fromArray(x);
            data_size = x.rows;
        } else if (!(x instanceof Matrix)) {
            for (const k of Object.keys(x)) {
                x[k] = Matrix.fromArray(x[k]);
                data_size = x[k].rows;
            }
        } else {
            data_size = x.rows;
        }

        for (const l of this._layers) {
            l.bind({input: x, supervisor: t, n: data_size, ...options});
        }
        const o = [];
        const r = {};
        for (let i = 0; i < this._layers.length; i++) {
            const l = this._layers[i];
            o[i] = l.calc(...l.parent.map(p => p.subscript !== null ? o[p.index][p.subscript] : o[p.index]));
            if (out && out.indexOf(l.name) >= 0) {
                r[l.name] = o[i];
                if (Object.keys(r).length === out.length) {
                    return r;
                }
            }
            if (!t && l instanceof NeuralnetworkLayers.output) {
                if (out) return r;
                return o[i];
            }
        }
        if (out) return r;
        return o[o.length - 1];
    }

    grad(e) {
        const bi = [];
        let bi_input = null;
        for (let i = 0; i < this._layers.length; bi[i++] = []);
        bi[bi.length - 1] = [new Matrix(1, 1, 1)];
        for (let i = this._layers.length - 1; i >= 0; i--) {
            const l = this._layers[i];
            if (e) {
                if (l instanceof NeuralnetworkLayers.output) {
                    bi[i] = [e];
                    e = null;
                } else {
                    continue;
                }
            }
            if (bi[i].length === 0) continue;
            let bo = l.grad(...bi[i]);
            if (!Array.isArray(bo)) {
                bo = Array(l.parent.length).fill(bo);
            }
            l.parent.forEach((p, k) => {
                if (!bo[k]) return;
                const subidx = p.subscript || 0;
                if (!bi[p.index][subidx]) {
                    bi[p.index][subidx] = bo[k].copy();
                } else {
                    bi[p.index][subidx].add(bo[k]);
                }
            });
            if (l instanceof NeuralnetworkLayers.input) {
                bi_input = bi[i][0]
            }
        }
        return bi_input;
    }

    update(learning_rate) {
        for (let i = 0; i < this._layers.length; i++) {
            this._layers[i].update(learning_rate);
        }
    }

    fit(x, t, epoch = 1, learning_rate = 0.1, options = {}) {
        if (Array.isArray(x)) {
            x = Matrix.fromArray(x);
        } else if (!(x instanceof Matrix)) {
            for (const k of Object.keys(x)) {
                x[k] = Matrix.fromArray(x[k]);
            }
        }
        t = Matrix.fromArray(t);

        let e;
        while (epoch-- > 0) {
            e = this.calc(x, t, null, options);
            this.grad();
            this.update(learning_rate);
        }
        return e.value;
    }
}

ノード

グラフ構造を考えたときに、出ていく枝へは順伝播、入ってくる枝へは逆伝播することになります。
なので、最低限各ノードには順伝播・逆伝播それぞれの処理を実装する必要があります。
また、ハイパーパラメータ設定、パラメータ更新、状態の保存・読み込みなども行う必要があるので、全てのノードの親クラスとして以下のクラスを用意しました。

class Layer {
    bind(x) {}

    calc(x) {
        throw new NeuralnetworkException("Not impleneted", this)
    }

    grad(bo) {
        throw new NeuralnetworkException("Not impleneted", this)
    }

    update(rate) {}

    get_params() {
        return null;
    }

    set_params(param) {}
}

実装自体は単純です。順伝播calcは単純に各ノード毎の処理を行ます。
また逆伝播gradはノードの処理を微分した(あるいは微分値の近似)値を引数に乗算します。いわゆる、自動微分ですね。
この二つを満たすことができれば、理論上はあらゆる計算処理を実装できます。

また、コンストラクタで初期パラメータの設定を、updateでパラメータの更新処理を行います。

なお、学習時の呼び出し順は次のように保証します。

  1. bind
  2. calc
  3. grad
  4. update

例えばSigmoidの計算を行うノードは以下のように定義します。

class SigmoidLayer extends Layer {
    constructor({a = 1}) {
        super();
        this._a = a;
    }

    calc(x) {
        this._o = x.copyMap(v => 1 / (1 + Math.exp(-this._a * v)));
        return this._o;
    }

    grad(bo) {
        const bi = this._o.copyMap(v => v * (1 - v));
        bi.mult(bo);
        return bi;
    }
}

また、全結合層は次のようになります。

class FullyConnected extends Layer {
    constructor({in_size = null, out_size, activation = null, l2_decay = 0, l1_decay = 0}) {
        super();
        this._in_size = in_size;
        this._out_size = out_size;
        this._w = null;
        this._b = Matrix.randn(1, out_size);
        if (activation) {
            this._activation_func = new NeuralnetworkLayers[activation]
        }
        this._l2_decay = l2_decay;
        this._l1_decay = l1_decay;
    }

    calc(x) {
        if (!this._w) {
            this._w = Matrix.randn(x.cols, this._out_size);
        }
        this._i = x;
        this._o = x.dot(this._w);
        this._o.add(this._b);
        if (this._activation_func) {
            return this._activation_func.calc(this._o);
        }
        return this._o;
    }

    grad(bo) {
        this._bo = bo;
        if (this._activation_func) {
            this._bo = this._activation_func.grad(bo);
        }
        this._bi = this._bo.dot(this._w.t);
        return this._bi;
    }

    update(rate) {
        const dw = this._i.tDot(this._bo);
        dw.mult(rate / this._i.rows);
        if (this._l2_decay > 0 || this._l1_decay > 0) {
            for (let i = 0; i < dw.rows; i++) {
                for (let j = 0; j < dw.cols; j++) {
                    const v = this._w.at(i, j)
                    dw.addAt(i, j, (v * this._l2_decay + Math.sign(v) * this._l1_decay) * rate);
                }
            }
        }
        this._w.sub(dw);
        const db = this._bo.sum(0);
        db.mult(rate / this._i.rows);
        this._b.sub(db);
    }

    get_params() {
        return {
            w: this._w,
            b: this._b
        }
    }

    set_params(param) {
        this._w = param.w.copy();
        this._b = param.b.copy();
    }
}

なお実装したノードクラスは連想配列を使用して、I/Fで示したtypeと対応付けをしておきます。

Worker

これだけでも動くのですが、さすがにこの処理をメインスレッドで実行するのは気が引けます。
なので、Workerスレッドで実行できるようにするため、次も用意しておきます。

importScripts('../js/math.js');
importScripts('../js/neuralnetwork.js');

self.model = {};
self.epoch = {};

self.addEventListener('message', function(e) {
    const data = e.data;
    if (data.mode == 'init') {
        const id = Math.random().toString(32).substring(2);
        self.model[id] = new NeuralNetwork(data.layers, data.loss);
        self.epoch[id] = 0;
        self.postMessage(id);
    } else if (data.mode == 'fit') {
        const samples = data.x.length;
        if (samples == 0) {
            self.postMessage(null);
            return;
        }

        const loss = self.model[data.id].fit(data.x, data.y, data.iteration, data.rate, data.options);
        self.epoch[data.id] += data.iteration;
        self.postMessage({
            epoch: self.epoch[data.id],
            loss: loss,
        });
    } else if (data.mode == 'predict') {
        const samples = data.x.length;
        if (samples == 0) {
            self.postMessage([]);
            return;
        }

        const y = self.model[data.id].calc(data.x, null, data.out, data.options);
        if (y instanceof Matrix) {
            self.postMessage(y.toArray());
        } else {
            for (const k of Object.keys(y)) {
                y[k] = y[k].toArray();
            }
            self.postMessage(y);
        }
    } else if (data.mode === 'close') {
        delete self.model[data.id];
    } else if (data.mode === 'copy') {
        const id = Math.random().toString(32).substring(2);
        self.model[id] = self.model[data.id].copy();
        self.epoch[id] = 0;
        self.postMessage(id);
    }
}, false);

使用しているもの

MLP:mlp.js
Autoencoder:autoencoder.js
GAN/Conditional GAN:gan.js
VAE/Conditional VAE:vae.js
DQN/DDQN:dqn.js

おわりに

なぜJSON構造でニューラルネットワークの構造を完全に作る必要があるのか。
JavaScriptにもクラスの概念があるのだから、それで作ればいいじゃん。(つまりKerasみたいな感じです)
など、思うかもしれません。

確かにその通りです。
ですがそもそもこの仕様は、APIによるニューラルネットワークモデル構築の構想を練っていた時期があり、その時のアイディアを基にしています。
なので、少々冗長ですがこういった形にしています。Workerでも実行しやすいですしね。

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

初学者向けAjaxまとめ 実例コード付き(GoogleMapsAPI)

はじめに

Ajaxとは、「JavaScriptを使って非同期でサーバーと通信を行う手法」です。(読みはエイジャックス)
この記事では基本動作の流れ、重要用語の説明を記述しています。初学者の方の理解の助けになればと思います。稚拙ではありますがGoogle Maps APIを使ったミニアプリも実例として、載せていますので、誰かの助けになれば嬉しいです。

AjaxはAsynchronous JavaScript + XML の略称。
※ Asynchronous【発音:ア・シンクロナス】 シンクロ(同期)の否定と思えばイメージが掴みやすいかな。

Ajax基本動作の流れ

①任意のボタンでイベント発生
②JavaScript + XMLHttpRequestでサーバーに対してリクエストを送信(非同期処理)
③サーバーで受け取った情報を処理
④処理結果はJSONやXMLなどの形式で応答
⑤受信したレスポンスを受けて、DOMでページを更新。

上記に出てきた用語をきちんと理解できると何をやっているのか見えてくると思います。
各用語について説明していきます。

用語説明

同期

クライアントがHTTPリクエストを送った後、HTTPレスポンスがサーバーから返ってくるまで待機することになる。ネットワークが遅い場合に、ページが真っ白になることがある。あの状態。ユーザーはその間操作不可。

HTTPリクエスト:クライアントからwebサーバーへの通信
HTTPリクエスト:webサーバーからクライアントへの応答
参考:https://itsakura.com/network-http-get-post

非同期

HTTPリクエストを送った後、放置して他の作業を進めておき、HTTPレスポンスが返ってきたタイミングで、その後の処理を再開する。ページ全体の動作を止めることがなく、ページの一部を更新することが可能。ユーザーを待たせるストレスを感じることなく、他の作業を進めることができる。

DOM

Document Object Model「ドキュメントオブジェクトモデル」の略。HTML文書およびXML文書のためのAPI。JavaScriptからHTMLを自在に操作する仕組みのこと。HTMLやXMLを「ツリー構造」として展開し、アプリケーション側に文章の情報を伝え、加工や変更をしやすくする。

API:
Application Programing Interfaceの略。アプリケーションをプログラミングするための何かとつなぐインターフェース。APIがソフトウェアやアプリケーションなどの一部を外部に向けて公開することで、第三者が開発したソフトウェアと機能を共有できるようにするもの。

XMLHttpRequest(XHR)

JavaScriptでHTTP通信を行うためのAPI。XMLHttpRequestはWEBサーバから一度すべてのデータを読み込んだ後でもデータの送受信を行うことができるため、ページをリロードすることなくページ内容を書き換えることが可能。

JSON

JavaScript Object Notationの略。JavaScript のデータ定義文をベースとした、簡易的なデータ定義言語。JSONは文字列のため、ネットワークを通してデータを転送したい場合に便利。JSONデータへアクセスしたい場合は、JavaScriptオブジェクトへ変換する必要があるが、JavaScriptにはJSONとJavaScriptオブジェクトを相互に変換できるJSONメソッドがある。以前は共通データ定義言語としてXMLが利用されてきたが、簡易的なJSONが利用されるケースが増えてきている。

参考:JSONについて
http://www.tohoho-web.com/ex/json.html
https://developer.mozilla.org/ja/docs/Learn/JavaScript/Objects/JSON
https://techplay.jp/column/611

実例(Ajaxを用いた通信)

このミニアプリの動作の流れ
①ユーザーがフォームに郵便番号を入力
②自動入力ボタンを押すとイベント発生(setState())。Ajaxを用いて、Google Maps APIへリクエスト。
③レスポンスが正しく返ってきたら、住所をフォームに自動入力し、マップを表示。

例_東京タワーの住所で検索
CSSはさぼってます
image.png

実務未経験のためコードは荒いですが、動作は確認済みです。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>郵便番号検索</title>
    <!-- viewport meta -->
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<style>
h1 {
    padding:30px;
}
#map {
    height: 300px;
}
</style>
<body>
    <div class="container">
        <div class="row justify-content-center">
            <h1>郵便番号検索</h1>
            <div class="col-md-12">
                <div class="content">
                    <label for="zip" class="col-md-4  text-md-right">郵便番号</label>
                    <input id="zip" name="zip" type="text">
                    <input type="button" value="自動入力" onClick="setState()">
                </div>
                <div class="content">
                    <label for="state" class="col-md-4 text-md-right">都道府県</label>
                    <input id="state" name="state" type="text">
                </div>
                <div class="content">
                    <label for="city" class="col-md-4 text-md-right">市町村</label>
                    <input id="city" name="city" type="text">
                </div>
                <div class="content">
                    <label for="address" class="col-md-4 text-md-right">番地</label>
                    <input id="address" name="address" type="text">
                </div>
            </div>
            <h1>地図表示</h1> 
            <div class="col-md-12">   
                <div class="content">
                    <label for="addressAll" class="col-md-4 text-md-right">表示している住所</label>
                    <span id="addressAll" name="addressAll" type="text"></span>
                    <div id="map"></div>
                </div>
            </div>
        </div>
    </div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<script src="https://maps.googleapis.com/maps/api/js?key={your_API_key}&callback=initMap" async defer></script> 

<script>
//郵便番号から住所を取得
function setState() {
    var zip = $('#zip').val();
    //バリデーション
    var pattern = /^\d{3}-?\d{4}$/g;
    var result = zip.match(pattern);
    //入力異常時の処理
    if((zip == "") || (result == null) ){
        alert('再度入力ください');
    }
    $.ajax({
        type : 'get',
        url : 'https://maps.googleapis.com/maps/api/geocode/json',
        crossDomain : true,
        dataType : 'json',
        data : {
            address : zip,   //APIへ送る郵便番号のデータ
            language : 'ja',
            sensor : false,
            key : '************************'
        },
        success:function(response){
            if(response.status == "OK") {
                ////--------   住所検索・表示     --------/////
                var obj = response.results[0].address_components;
                //値を配列に格納
                var value = [];
                for (var i=1; i<=3; i++) {
                    value[i] = obj[i]['long_name'];
                }
                //値を表示
                $('#state').val(value[3]);   // 都道府県
                $('#city').val(value[2]);    // 市町村名
                $('#address').val(value[1]); // 番地
                $("#addressAll").empty();    //値の中身を削除(複数回検索すると連なるため)
                $('#addressAll').append(""+zip+" "+value[3]+value[2]+value[1]);
                //mapを生成する関数を呼び出す
                initMap();
            }else{
                alert('住所情報が取得できませんでした');
            }
            ////--------   マップ表示     --------/////
            function initMap() {
                 // 緯度・経度を変数に格納 
                var lat = response.results[0].geometry.location.lat;
                var lng = response.results[0].geometry.location.lng;
                //表示するマップのセンター
                var center = new google.maps.LatLng(lat,lng);
                //マップのオプション
                var options = {
                    zoom: 18,  //地図のズームを指定
                    center: center
                };
                //地図を表示
                var map = new google.maps.Map(document.getElementById("map"), options);
                // マップにマーカーを表示
                var marker = new google.maps.Marker({
                                map : map,  
                                position : center
                });
            }
        }
    })
}
</script>
</body>
</html>

⭐️困った時用の殴り書きメモ
Google Maps APIのレスポンス
・statusは検索結果のステータス。検索結果の成功と失敗を判定。
・resultsは検索結果の情報を配列として持っている。
・geometryは場所の情報を持つlocation、検索対象範囲を示すviewportなどで構成。
・オブジェクトの中身を取り出していく時はconsole.log()で中身を確認して取得していけば難しくはない。
・レスポンスが返ってこなければ、APIキーの制限に問題ないか確認。

参考:レスポンスで返ってくるオブジェクト
https://developers.google.com/maps/documentation/geocoding/overview?hl=ja#ComponentFiltering
https://vintage.ne.jp/blog/2015/04/395
参考:Google Maps APIキー制限
https://maps.multisoup.co.jp/blog/2967/
その他参考:
https://www.codegrid.net/articles/2014-ajax-1/#toc-4
https://api.jquery.com/jQuery.ajax/

最後に

理解をより深めるために初学者が自分用にまとめました。
AjaxはAPIと通信するときに使うと思うので学習必須だと思っています。
今後も記事を更新して、より深みを出していきます。
(もしこの記事に誤りがありましたらご教授いただけると幸いです。)

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

Reactの関数コンポーネントの基本をまとめてみた

(まだ、完全にまとまってません、これから追記していきます)
業務でReactを使って1年以上経ちますが、数ヶ月前から新しく作るコンポーネントは関数コンポーネント+TypeScriptに移行しました。
元々はClassコンポーネントで書いていたのですが、なぜ関数コンポーネントが優れているのかを知っておきたかったのと、現場でベテランエンジニアから学んだ書き方のTipsなどを記事にしようと思います。

関数コンポーネントとは?

Classに比べると簡潔に見通しよく書けるメリットがあったものの、Classの様にStateを持つ事ができなかった。(当たり前ですが、関数は状態を持つことができません。関数の実行が終われば、関数内の変数はスコープから外れ、アクセスできなくなります、クロージャーとかありますが、、)

そこで考案されたのがフックです。フックは、関数コンポーネントの中からReactの機能へ接続(Hooks into)することを実現しました。フックを用いることで、関数コンポーネントにおいてもクラスコンポーネントとほぼ同等の機能を実現することができる様になったわけです。

これによって、関数コンポーネントが主流になったらしいです。

では、フックとは?

フックは、関数コンポーネントに state やライフサイクルといった React の機能を “接続する (hook into)” ための関数です。フックは React をクラスなしに使うための機能ですので、クラス内では機能しません。今すぐに既存のコンポーネントを書き換えることはお勧めしませんが、新しく書くコンポーネントで使いたければフックを利用し始めることができます。
- React公式ドキュメントから抜粋

ですので、私がフロント開発に携わるサービスもClassコンポーネントが残っていて混在してる状態です。

では、基本のフックを見ていきましょう。

・ステートフック useState
その名の通り、state(状態)を持つ事ができる様になります。

最も単純なケースで表示、非表示を切り替えるboolean変数(showMenu)を作りましょう。
お作法として変数を書き換えるメソッド名はset + 変数名です。

const [showMenu, setShowMenu] = useState<boolean>(false);
const toggleVisibility = () => setShowMenu(prev => !prev);

prev => !prevと書いてるのは、現在の状態 => 現在の状態の逆 という事です。

・副作用フック useEffect
Classでいう、ComponentDidMount、ComponentDidUpdateを行う事ができる。
第一引数にcallbackを入れて、第二引数に依存する値の配列を入れる
* 依存する値が変更される度にcallbackが実行される

useEffect(
  () => {
     実行したい処理
  },
  [ 依存する値/state ]
);

・メモ化フック
同じ結果を返す処理に関しては初回のみ処理を実行しておき、2回目以降は前回の処理結果を呼び出すことで毎回同じ処理を実行しなくてよくなります。これはプログラミングではメモ化と呼ばれるテクニックで、それをReact Hooks上で簡単に利用できるのがuseMemoとuseCallbackです。
パフォーマンス向上のためにuseMemo、useCallbackは使われる。
パフォーマンス向上のポイントは 無駄な計算を抑え 、再レンダリングをできるだけ抑える この2つです。

useMemoは、計算結果を記憶し、必要な時だけ再計算することができる機能
useCallbackは、子コンポーネントに渡すコールバック関数を記憶しておく事ができる機能です

まとめ

関数コンポーネントのメリットは、constructor, renderなどが不要で記述量を減らせるからこっちでやるべき。

参考

https://www.to-r.net/media/react-tutorial-hooks-usememo-usecallback/
https://times.hrbrain.co.jp/entry/react-hooks-performance
https://sbfl.net/blog/2019/11/12/react-hooks-introduction/

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

jquery(private, publicプロパティ)

初めに

 ジャバスクリプトのprivate, publicプロパティがあります。

private, publicプロパティに対して

①クラスを定義する
②private プロパティ、public プロパティを定義する
③ボタンをクリックすると、クラスのインスタンスを生成し、public プロパティをよぶ

①HTML分に以下のHTMLソースを追加する

<html>

  <head>
    <title>Hoge</title>
  </head>

  <body id="main-id">
  </body>

</html>

②JQUERYを選択してから、JAVASRIPT分に以下のソースを追加する

// マインオブジェクト
var $main  = $("#main-id");

// クラス定義
var Hoge = function() {

  // private プロパティ
  var privateString = "サンプルテキスト";

  // public プロパティの場合、 thisを使う
  this.privateString = privateString;
};

// ボタンオブジェクト
var $button = $("<button>").html("hoge");

// ボタンクリックイベント
$button.click(function() {

   // インスタンス作成
   var hoge = new Hoge();

   // public プロパティを呼ぶ
   alert(hoge.privateString);
});

//親オブジェクトに子オブジェクトを追加
$main.append($button);

実行
スクリーンショット 2020-12-23 13.31.10.png

private, public関数に対して

①クラスを定義する
②private 関数、public 関数を定義する
③ボタンをクリックすると、クラスのインスタンスを生成し、public 関数をよぶ

①HTML分に以下のHTMLソースを追加する

<html>

  <head>
    <title>Hoge</title>
  </head>

  <body id="main-id">
  </body>

</html>

②JQUERYを選択してから、JAVASRIPT分に以下のソースを追加する

// マインオブジェクト
var $main  = $("#main-id");

// クラス定義
var Hoge = function() {

  // private プロパティ
  var privateString = "サンプルテキスト";

  // public プロパティの場合、 thisを使う
  this.privateString = privateString;

  // private 関数
  var show = function() {
    alert(privateString);
  };

  // public 関数の場合、thisを使う
  this.show = function() {
      return show();
  }
};

// ボタンオブジェクト
var $button = $("<button>").html("hoge");

// ボタンクリックイベント
$button.click(function() {

   // インスタンス作成
   var hoge = new Hoge();

   // public 関数を呼ぶ
   hoge.show();
});

//親オブジェクトに子オブジェクトを追加
$main.append($button);

実行
スクリーンショット 2020-12-23 13.35.56.png

public関数に対してprototypeもできます。

例)

// クラス定義
var Hoge = function() {

  // private プロパティ
  var privateString = "サンプルテキスト";

  // publicの場合、 thisを使う
  this.privateString = privateString;


  // private 関数
  var show = function() {
    alert(privateString);
  };

  // public 関数の場合、thisを使う
  this.show = function() {
      return show();
  }
};

↓ 上の方法の代わりに、prototypeもできます

// クラス定義
var Hoge = function() {

  // private プロパティ
  var privateString = "サンプルテキスト";

  // public プロパティの場合、 thisを使う
  this.privateString = privateString;
};

// public 関数の場合
Hoge.prototype.show = function() {
    alert(this.privateString);
};

結果
スクリーンショット 2020-12-24 16.22.53.png

以上

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

jqueryのタグまとめ

初めに

jQueryを使う上で最低限知ってないと読めない知識の寄せ集めです。
HTMLの代わりに、 jQueryオブジェクトが反映される

html要素・jQueryの指定

① (div)ディブオブジェクト

html要素)

<div>
    <p>サンプルテキスト</p>
    <div>ディブ</div>
</div>

↓ jQueryオブジェクトに変換する

//divオブジェクトに変換する
var $divParent = $("<div>"); 

//pオブジェクトに変換する
var $p = $("<p>").html("サンプルテキスト");

//divオブジェクトに変換する
var $divChild = $("<div>").html("ディブ");

//親オブジェクトに子オブジェクトを追加
$divParent.append($p).append($divChild);

結果
スクリーンショット 2020-12-24 8.37.34.png

②(input)インプットオブジェクト

html要素)

<div>
    <p>サンプルテキスト1</p>
    <span>サンプルテキスト2</span>
    <input type="text" />
</div>

↓ jQueryオブジェクトに変換する

//divオブジェクトに変換する
var $div = $("<div>");  

//pオブジェクトに変換する
var $p = $("<p>").html("サンプルテキスト1");

//spanオブジェクトに変換する
var $span = $("<span>").html("サンプルテキスト2");

//inputオブジェクトに変換する
var $input= $("<input>");

//親オブジェクトに子オブジェクトを追加
$div.append($p).append($span).append($input);

結果
スクリーンショット 2020-12-24 8.42.00.png

③(link)リンクオブジェクト

html要素)

<div>
    <h1>タイトル</h1>
    <p>サンプルテキスト</p>
    <a href="#">リンク</a>
</div>

↓ jQueryオブジェクトに変換する

//divオブジェクトに変換する
var $div = $("<div>");

//h1オブジェクトに変換する
var $h1 = $("<h1>").html("タイトル");

//pオブジェクトに変換する
var $p = $("<p>").html("サンプルテキスト");

//inputオブジェクトに変換する
var $a = $("<a href='#'>").html("リンク");

//親オブジェクトに子オブジェクトを追加
$div.append($h1).append($p).append($a);

結果
スクリーンショット 2020-12-24 8.44.48.png

④( table)テーブルオブジェクト

html要素)

<div>
    <table>
        <tr>
            <td>
                 サンプルテキスト
            </td>
        <tr>
    </table>
</div>

↓ jQueryオブジェクトに変換する

//divオブジェクトに変換する
var $div = $("<div>");

//tableオブジェクトに変換する
var $table = $("<table>");

//trオブジェクトに変換する
var $tr = $("<tr>");

//tdオブジェクトに変換する
var $td = $("<td>").html("サンプルテキスト");

//親オブジェクトに子オブジェクトを追加
$div.append($table.append($tr.append($td)));

結果
スクリーンショット 2020-12-24 8.48.22.png

⑤ ulオブジェクト

html要素)

<div>
    <ul>
        <li>サンプルテキスト1</li>
        <li>サンプルテキスト2</li>
    </ul>
</div>

↓ jQueryオブジェクトに変換する

//divオブジェクトに変換する
var $div = $("<div>");

//ulオブジェクトに変換する
var $ul = $("<ul>");

//liオブジェクトに変換する
var $li1 = $("<li>").html("サンプルテキスト1");

//liオブジェクトに変換する
var $li2 = $("<li>").html("サンプルテキスト2");

//親オブジェクトに子オブジェクトを追加
$div.append($ul.append($li1).append($li2));

結果
スクリーンショット 2020-12-24 8.50.32.png

⑥(combobox)選択ボックスオブジェクト

html要素)

<div>
   <select class="select">
       <option>サンプルテキスト1</option>
       <option>サンプルテキスト2</option>
   </select>
</div>

↓ jQueryオブジェクトに変換する

//divオブジェクトに変換する
var $div = $("<div>");  

//selectオブジェクトに変換する
var $select = $("<select>").addClass("select");

//optionオブジェクトに変換する
var $option1 = $("<option>").html("サンプルテキスト1");

//optionオブジェクトに変換する
var $option2 = $("<option>").html("サンプルテキスト2");

//親オブジェクトに子オブジェクトを追加
$div.append($select.append($option1).append($option2));

結果
スクリーンショット 2020-12-24 8.52.58.png

以上

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

TypeORMで1:1のリレーションの定義の仕方

TypeScriptのメジャーなORM(ORマッパー)であるTypeORMには、cliコマンド経由でmigrationファイルを自動生成する機能があります。

めちゃめちゃ便利なのですが、ドキュメントが不足気味のため、少し複雑なパターンの時に実装方法がわからない事があります。今回、1:1のリレーションの定義方法でハマったので、試行錯誤してわかったやり方を共有します。

やりたいこと

親テーブル(employee)

id 備考
id pkey
name

子テーブル

id 備考
employee_id pkey、employee.idへのfkey
tag

このような、

  • 1:1のリレーション
  • 子テーブルのpkeyが、親テーブルのpkeyへのfkeyになっている

というパターンをtypeORMのmigartion自動生成機能(generating-migarations)で生成します。

正解

TypeORM - Amazing ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Electron platforms.

tsファイル

employee.ts
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
import { EmployeeChild } from './employee-child.entity';

@Entity('employees')
export class Employee {
  @PrimaryGeneratedColumn({ type: 'int', name: 'id' })
  id: number;

  @Column('varchar', { name: 'name', length: 255 })
  name: string;

  @OneToOne(
    () => EmployeeChild,
    (employeeChild) => employeeChild.employee,
  )
  employeeChild: EmployeeChild;
}
employee-child.ts
import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
import { Employee } from './employee.entity';

@Entity('employee_children')
export class EmployeeChild {
  @PrimaryColumn('int', { name: 'employee_id' })
  @OneToOne(
    () => Employee,
    (employee) => employee.employeeChild,
  )
  @JoinColumn([{ name: 'employee_id', referencedColumnName: 'id' }])
  employee: Employee;

  @Column('varchar', { name: 'tag', length: 255 })
  tag: string;
}

ポイントは、親テーブルの方にJoinColumnを書かない事です。

  @OneToOne(
    () => EmployeeChild,
    (employeeChild) => employeeChild.employee,
  )
  @JoinColumn([{ name: 'id', referencedColumnName: 'employee_id' }])
  employeeChild: EmployeeChild;

とすると一見良さそうですが、

Referenced column employee_id was not found in entity EmployeeChild

というエラーが発生してうまくいきません。

生成されたmigrationファイル

import { MigrationInterface, QueryRunner } from 'typeorm';

export class test1610269363257 implements MigrationInterface {
  name = 'test1610269363257';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      'CREATE TABLE `employees` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB',
      undefined,
    );
    await queryRunner.query(
      'CREATE TABLE `employee_children` (`employee_id` int NOT NULL, `tag` varchar(255) NOT NULL, UNIQUE INDEX `REL_379a2d987c438f3096c06a6ebd` (`employee_id`), PRIMARY KEY (`employee_id`)) ENGINE=InnoDB',
      undefined,
    );
    await queryRunner.query(
      'ALTER TABLE `employee_children` ADD CONSTRAINT `FK_379a2d987c438f3096c06a6ebd5` FOREIGN KEY (`employee_id`) REFERENCES `employees`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION',
      undefined,
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      'ALTER TABLE `employee_children` DROP FOREIGN KEY `FK_379a2d987c438f3096c06a6ebd5`',
      undefined,
    );
    await queryRunner.query(
      'DROP INDEX `REL_379a2d987c438f3096c06a6ebd` ON `employee_children`',
      undefined,
    );
    await queryRunner.query('DROP TABLE `employee_children`', undefined);
    await queryRunner.query('DROP TABLE `employees`', undefined);
  }
}

よく見るとemployee_idに対して、PRIMARY KEYUNIQ INDEXの両方が設定されています。

UNIQUE INDEXの方はいらないはずですが、消し方はわかりませんでした。(あってもパフォーマンスへの影響以外は無いと思われます)

  @PrimaryColumn('int', { name: 'employee_id', unique: false })

としてみましたが結果は変わらず。

備考

TypeORM 0.2.25
MySQL 5.7

で確認しました。

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

[React] firestoreのデータをaxios取得に書き換える

開発環境

react : 17.0.1
node : 12.18.2
npm : 6.14.5
firebase : 8.2.1
axios : 0.21.1

既存コード

今回はusersコレクションをfirestoreで用意しておき、userのデータをfirestoreから取得し、userのnameを一覧で表示させる実装です。

top.js
import React, { useState, useEffect } from 'react'
import firebase from '../lib/firebase'

const Top = () => {
  const [users, setUsers] = useState([])

  // firestoreからuserのデータを取得する関数
  const getFirestoreUsers = async () => {
    const snapshot = await firebase.firestore().collection('users').get()
    const users = snapshot.docs.map(doc => doc.data())
    setUsers(users)
  }

  useEffect(() => {
    getFirestoreUsers()
  }, [])

  return (
    <div>
      {
        users.map((user, index) => (
          <p key={index}>{user.name}</p>
        ))
      }
    </div>
  )

export default Top

axiosの導入

yarn add axios

axiosでfirestoreのデータを取得する

userShow.js
import React, { useState, useEffect } from 'react'
import axios from 'axios'

const UserShow = () => {
  const [users, setUsers] = useState([])

  useEffect(() => {
    // プロジェクトIDは環境変数で呼び出す
    // process.envを付けるのを忘れずに
    const USERS_URL = `https://firestore.googleapis.com/v1/projects/${process.env.REACT_APP_PROJECT_ID}/databases/(default)/documents/users`
    const fetchUsersData = async () => {
      const result = await axios(USERS_URL)       

      setUsers(result.data.documents)
    }
    fetchUsersData()
  }, [])

  return (
    <div>
      {
        users.map((user, index) => (
          <p key={index}>{user.fields.name.stringValue}</p>
        ))
      }
    </div>
  )
}

export default UserShow

まとめ

viewで呼び出す記述が長い気がするので、他に良い実装方法があれば教えていただきたいです。。

参考記事

Cloud Firestore REST API を使用する
How to fetch data with React Hooks?

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

DB 操作まで JS で完結!Next.js × Prisma を CRUD アプリケーションでざっくり理解する。

何について書いた記事か?

  • Next.js × Prisma を利用して CRUD アプリケーションを作成した ので、そのまとめ記事です.
    • :beginner: Next.js は React でアプリケーションを作成するためのフレームワークです.
    • :beginner: Prisma は Node.js のための ORM です.
  • 各レイヤーの役割とソースコードを対応付けながら説明することで、Next.js × Prisma で作るとどんな実装になるのか?』 をざっくりと理解することを目指します.
  • 開発を始める前にこの記事で概要を掴む 的な使い方をしてくれると嬉しいです :smile:

:bulb: こんな人にオススメ

  • Next.js / Prisma に興味を持っていて、まずはこれらで実現できることを知りたい.
  • React / Vue.js を書いていて、バックエンドも JS (TS) で書けたらいいなと思っている.

:warning: 注意点

  1. サンプルアプリケーション開発のハンズオン的な説明は行っていません.
  2. 自分は React, TypeScript の経験が浅いので、慣例 / ベストプラクティスに沿っていないソースがある と思います :rolling_eyes:
    • 逆に言えば、慣れていなくても楽しく、ストレスなく開発できました!
    • 『ここはこう書いた方がいいよ!』といったご指摘があれば、コメントで教えていただけると嬉しいです :bow:

何を作ったか?

シンプルに CRUD できるだけのアプリケーションです.

next-prisma-crud-demo.gif

  • 題材は何でもよかったので、スターバックスのカスタマイズドリンクを CRUD する前提にしています.

chanmio_special.jpg

  • 認証は入れていないので好きにデータ編集してもらってOKです.
    • (不正利用対策は主目的でないので、常識の範囲内で抑えてくれると助かります :pray: )
  • :warning: 性能: ローカル開発では気になりませんでしたが、デプロイすると DB との通信に結構時間がかかるようになってしまった (特に初回通信) ので、Vercel か AWS RDS で追加で設定すべき項目があるかもしれません.

Next.js × Prisma で作ったアプリケーションの構成

新しい技術を学ぶときの難しさの一つに 『色んな登場人物が出てきて関係性がわからない :innocent: ということがあると思っているので、最初に アプリケーションの主要な構成 を説明しておきます.

ただ、この説明だけでそれぞれについて深く理解することは難しいと思うので、まずは 『なるほど、こういうものがあるのね :thinking: と把握してもらえれば OK です.

:star: WEB ページにアクセスされてから DB 操作までのフロー

next-prisma-app-structure.png

  • 図の :arrow_up: が開発環境 / :arrow_down: が本番環境です.
    • 本番での差分にはオレンジの枠線を付けています.
  • フロントエンドは React による実装で、UI コンポーネントとして chakra-ui を利用しました.
  • バックエンドを Next.js の API Routes で実装しています.
    • これにより、フロントエンド / バックエンドがどちらも Next.js 上で実行される ことになります.
  • フロントエンド ⇄ バックエンドの通信は axios で行います.
  • バックエンド ⇄ DB の通信は Prisma で行います.

CRUD ごとにソースコードを理解する

前置きが長くなってしまいましたが、ようやく Next.js / Prisma のコードに関する説明です.
まずは CRAETE を例に、 React コンポーネント ⇄ API Routes ⇄ Prisma の関連図とソースコード を見ていきます.
READ / UPDATE / DELETE については、Prisma のコード例だけ紹介しておきます.

:warning: Prisma の基本的な書き方を集中するため、エラーハンドリングは省略しています.

:white_check_mark: CREATE

:star: CREATE するまでのフロー

next-prisma-create.png

:star: CRAETE を行う React コンポーネント

pages/create.tsx
export default function Create() {
  ...

  const createBeverage = async (values: BeverageFormData, actions) => {
    // Formik からフォームに入力された値を取得
    const body = {
      name: values.name,
      description: values.description,
      price: Number(values.price),
      isRecommend: values.isRecommend,
    };

    // axios で API Routes として定義された URL に通信を飛ばす
    await axios.post("/api/beverages", body);

    // 登録後のロジック
    ...
  };

:star: CRAETE を行う API Routes ( Prisma Client を利用 )

pages/api/beverages.ts
const handleCreate = async (
  req: NextApiRequest,
  res: NextApiResponse<Beverage>
) => {
  // request からフォームの値を取得
  const { name, description, price, isRecommend } = req.body;

  // 登録前のチェック
  ...

  // prisma - CREATE
  // Prisma Client を呼び出して、DB にデータを登録する.
  const beverage = await prisma.beverage.create({
    data: { name, description, price, isRecommend },
  });
  // 登録結果を JSON で返却する
  res.json(beverage);
};

:white_check_mark: READ

:star: READ を行う API Routes ( Prisma Client を利用 )

pages/api/beverages.ts
const handleRead = async (
  req: NextApiRequest,
  res: NextApiResponse<Beverage[]>
) => {
  // prisma - READ
  // テーブル内の全データを id 昇順に取得
  const beverages = await prisma.beverage.findMany({
    orderBy: {
      id: "asc",
    },
    // 取得データを絞り込む場合はココに `where` などを追加していく
    // https://www.prisma.io/docs/concepts/components/prisma-client/crud
  });
  res.json(beverages);
};

:white_check_mark: UPDATE

:warning: UPDATE / DELETE では、どのデータに対して操作するのかを特定するために、ファイル名を [id].ts として URL から情報を受け取っています.
ここでは、Next.js - Dynamic API Routes という仕組みを利用しています.

:star: UPDATE を行う API Routes ( Prisma Client を利用 )

pages/api/beverage/[id].ts
const handleUpdate = async (
  req: NextApiRequest,
  res: NextApiResponse<Beverage>
) => {
  // URL から id を取得
  const url = req.url;
  const updateID = parseInt(url.split(/\//, 10).pop());
  // request からフォームの値を取得
  const { name, description, price, isRecommend } = req.body;

  // 更新前のチェック

  // prisma - UPDATE
  // id が一致したデータに対して、フォームに入力された値で更新
  const beverage = await prisma.beverage.update({
    where: { id: updateID },
    data: { name, description, price, isRecommend },
  });
  res.json(beverage);
};

:white_check_mark: DELETE

:star: DELETE を行う API Routes ( Prisma Client を利用 )

pages/api/beverage/[id].ts
const handleDelete = async (
  req: NextApiRequest,
  res: NextApiResponse<Beverage>
) => {
  // URL から id を取得
  const url = req.url;
  const deleteID = parseInt(url.split(/\//, 10).pop());

  // prisma - DELETE
  // id が一致したデータを削除
  const beverage = await prisma.beverage.delete({
    where: {
      id: deleteID,
    },
  });
  res.json(beverage);
};

見ていただいた通り、Prisma を利用すると DB 操作が簡単に書けていい ですね!
また、スキーマ定義を元に型定義が生成されるので、タイプセーフに書ける というのも大きなメリットです.


Next.js × Prisma で開発するメリットは何か?

実際に開発して感じた Next.js × Prisma のメリットについてまとめておきます.

メリット :one: : JS (TS) だけで完結するため、キャッチアップコストを抑えられる

  • 当然ですが、フロントエンド / バックエンドが同一言語なので開発のキャッチアップコストを抑えることができます.
  • また、今回開発した構成だと 従来の REST API と構成が近いため、そういった意味でも理解しやすい のではと思っています.
  • React / Vue.js などのモダンフロントエンドに足を突っ込むとそれだけで結構なキャッチアップコストを支払うことになるので、『そこで学んだ技術でバックエンドも書きたい』というのは自然な発想で、それが実現できるという意味で 開発体験は非常によかった です.

メリット :two: : 同一言語 / 同一ディレクトリのため共通化がしやすい

  • 全てが JS で書かれることで、これまでフロントエンド / バックエンドで別々に管理されていた仕組みを簡単に共通化できるようになります.
  • 例えば、今回のアプリケーションではバリデーション定義を共通化したので、以下に実装例を載せておきます.

:white_check_mark: バリデーション定義の共通化

  • BeverageFormSchema.ts という Yup スキーマを定義して、それをフロントエンド / バックエンドそれぞれから import することで簡単にバリデーション定義を共通化することができました :smiley:
validators/BeverageFormSchema.ts
// Yup で入力チェックのためのスキーマを定義する
export const beverageFormSchema = Yup.object({
  name: Yup.string().required(),
  description: Yup.string(),
  price: Yup.number().min(0).max(3000),
  isRecomment: Yup.boolean(),
});

:star: Frontend

components/BeverageForm.tsx
// 1. スキーマをインポートして...
import { beverageFormSchema } from "../validators/BeverageFormSchema";

...

export default function BeverageForm({ ... }) {
  return (
    <Formik
      initialValues={initialValues}
      onSubmit={onSubmit}
      // 2. Formik に props として渡す
      validationSchema={beverageFormSchema}
    >

:star: Backend

pages/api/beverages.ts
// 1. スキーマをインポートして...
import { beverageFormSchema } from "../../validators/BeverageFormSchema";

...

const handleCreate = async ( ... ) => {
  const { name, description, price, isRecommend } = req.body;

  // 2. Yup の API を利用してバリデーションチェック
  const isValid = await beverageFormSchema.isValid({
    name,
    description,
    price,
    isRecommend,
  });
  if (!isValid) {
    res.status(400).end("sent param is invalid.");
    return;
  }

メリット :three: : サーバー管理が楽になる

  • yarn dev を実行して Next.js の開発用サーバーを立てれば SSR / ISR や API Routes もそこで実行されるため、フロントエンド / バックエンドで別々の開発用サーバーを管理しなくてよくなります.
  • 個人開発や小規模なアプリにとって、このコンパクトさは大きな魅力 だと思いました.

おわりに

いかがだったでしょうか.
Next.js × Prisma で開発をしてみて感じた良さが少しでも伝わっていれば嬉しいです.

この記事で『興味を持ったよ』という方は、是非以下のチュートリアルなどを参考に実際に開発をしてみることをオススメします.
結局、手を動かしてみるのが一番理解できると思うので!

以上、長い記事を読んでいただいてありがとうございました :bow:

開発に役立つチュートリアル集

  • Next.js - Create a Next.js App
    • Next.js の公式チュートリアルです.
    • Next.js の各機能についてハンズオン形式で解説されていて、図も豊富で非常に分かりやすかった です.
  • Prisma - Quickstart
    • Prisma を TS と SQLite で試すチュートリアルです.
    • Next.js のようなフレームワークと分離して Prisma 単体から理解を始めたい という方にオススメです.
  • Prisma - How to Build a Fullstack App with Next.js, Prisma, and PostgreSQL
    • つい最近 Prisma の公式ページに追加されていた記事なので自分はこれには沿っていないのですが、上で説明したものと同じ技術要素でのチュートリアルです.
    • プロジェクトの作成からデプロイまで丁寧に解説されていた ので、これに沿っていけば一通りの開発は順調に進められそうでした.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Next.js】next/routerを基礎からしっかり。

前書き

筆者がNext.jsを仕事で書くことになったので、1から勉強するためにアウトプット記事を書くことにしました。
基本的にはドキュメントを噛み砕いて、翻訳した記事です。間違っているところなどあれば、ご指摘していただけるとありがたいです?‍♂️

以下、本題です。

*前提知識として【Next.js】Routingを基礎からしっかり。を一読しておくと、理解しやすいかと

useRouter

useRouter hookを使うと、アプリ内のrouterオブジェクトにアクセスすることができます。

import { useRouter } from 'next/router'

const Hoge = ({ children, href }) => {
  const router = useRouter()

  const handleClick = (e) => {
     e.preventDefault()
     route.push(href)
  }

  return (
    <a href={href} onClick={handleClick}>
      { children }
    </a>
  )
}

Hogeコンポーネントの子要素をラップして、指定したhrefへのリンクコンポーネントを作成するコードです。

const router = useRouter()の部分でrouterオブジェクトを取得できます。具体的にrouterオブジェクトに定義されているメソッドは下記です。

router object

  • pathnameString => 現在のrouteを表示。/pages内のpathを表示します。
  • queryObject => オブジェクトにパースされたクエリ文字列を表示します。
  • asPathString => ブラウザに表示される(クエリを含む)pathを表示します。
  • basePathString => 使えるBasePathを表示する。

主な関数はこんな感じです。まだいくつかあるので、気になる方は調べてみてください。

router.push

クライアント側の遷移を処理します。このメソッドはnext/linkが苦手な部分をカバーくれます。

router.push(url, as, options)
  • url:ここに遷移します
  • as:ブラウザに表示されるURLのオプションのデコレータ
  • options:設定オプション
    • shallow:再実行せずに現在のページのパスを更新する

使い方

事前に定義されたルート(pages/about.js)への遷移

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return <span onClick={() => router.push('/about')}>Click me</span>
}

動的なルート(pages/post/[pid].js)への遷移

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return <span onClick={() => router.push('/post/abc')}>Click me</span>
}

pages/login.jsへリダイレクトさせたい場合。認証の後のページに便利。

import { useEffect } from 'react'
import { useRouter } from 'next/router'

const useUser = () => ({ user: null, loading: false })

export default function Page() {
  const { user, loading } = useUser()
  const router = useRouter()

  useEffect(() => {
    if (!(user || loading)) {
      router.push('/login')
    }
  }, [user, loading])

  return <p>Redirecting...</p>
}

With URL object

URLオブジェクトはnext/linkと同じように使うことができます。URLとパラメータの両方で使用できます。

import { useRouter } from 'next/router'

export default function ReadMore({ post }) {
  const router = useRouter()

  return (
    <span
      onClick={() => {
        router.push({
          pathname: '/post/[pid]',
          query: { pid: post.id },
        })
      }}
    >
      Click here to read more
    </span>
  )
}

オブジェクトの形でpathnamequeryを指定していることが確認できるかと思います。ここでは動的なルーティングを実現しています。

router.replace

router.replace(url, as, options)

それぞれの役目はrouter.pushと同じです。

next/linkreplacepropと同様に、URLエントリを履歴スタックに追加するのを防ぎます。わかりにくいので例をみていきましょう。

では、使い方を見ていきましょう。

使い方

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return <span onClick={() => router.replace('/home')}>Click me</span>
}

router.prefetch

クライアント側の遷移を高速化するためにページをプレフェッチします。とはいえnext/linkが自動的にページのプリフェッチを行うため、この方法はnext/linkを使用しないナビゲーションにのみ有用です。

ちなみにProduction環境での話であり、開発環境ではプレフェッチしません。

使い方

ログインページがあり、ログイン後にダッシュボードにリダイレクトするとします。その場合、次の例のように、ダッシュボードをプリフェッチすることで、より速く遷移させることができます。

つまりnext/linkでは自動的にプリフェッチしてくれるのですが、リダイレクト処理等でnext/routeを使う場合はプリフィッチしてあげると表示を高速にしてくれます。

import { useCallback, useEffect } from 'react'
import { useRouter } from 'next/router'

export default function Login() {
  const router = useRouter()
  const handleSubmit = useCallback((e) => {
    e.preventDefault()

    fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        /* Form data */
      }),
    }).then((res) => {
      // Do a fast client-side transition to the already prefetched dashboard page
      if (res.ok) router.push('/dashboard')
    })
  }, [])

  useEffect(() => {
    // Prefetch the dashboard page
    router.prefetch('/dashboard')
  }, [])

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      <button type="submit">Login</button>
    </form>
  )
}

基本的な使い方は以上です。ではでは〜

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

JavaScriptのスコープ内関数のホイスティングでハマった

ハマった内容

  • ブロックスコープ内の関数はブロックスコープ外から呼び出せる。(ブロックスコープの仕様)
  • 関数はホイスティングされるから、関数の定義より前に関数の呼び出しを書いても、問題なく呼び出せる。(ホイスティングの仕様)

と言うことは、ブロックスコープの外側で、ブロックスコープ内の関数定義より前に呼び出したら、呼び出せるのでは...?(馬鹿)

結論

ホイスティングは、スコープ内で巻き上げを行うものです。
スコープ外まで巻き上げないので、呼び出せません。
なので、Uncaught TypeError: fn is not a functionとなります。

ホイスティングとは

JavaScriptの仕様で、スコープの中で、変数や関数が先頭に巻き上げられることです。

具体例

Example
fn();

function fn() {
    console.log('hoge');
}

/**
 * 出力:hoge
 * 理由:ホイスティングされるので、関数の定義より前に関数を呼び出していても、関数は問題なく実行される。
**/

ブロックスコープとは

{...}で括られたスコープのことです。
ブロックスコープ内の let / const はブロックスコープ外から参照できません。
function は参照できます。

具体例

Example2
{
    let a = 0;
    function fn() {
        console.log('hoge');
    }
}
Example2-1
console.log(a);

/**
 * 出力(エラー):Uncaught ReferenceError: a is not defined
 * 理由:ブロックスコープ内の let / const はブロックスコープ外から参照できない
**/
Example2-2
fn();

/**
 * 出力:hoge
 * 理由:funtionは参照できる
**/

ハマったところ

Example3
fn();
{
    let a = 0;
    function fn() {
        console.log('hoge');
    }
}
/**
 * 出力(エラー):Uncaught TypeError: fn is not a function
 * 理由:ホイスティングはスコープ内が対象のため、ブロックスコープ定義より前からの呼び出しはできない
**/

まとめ

間違っていたら、コメントで教えていただけると嬉しいです。

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

JavaScriptで画像を表示する方法

JavaScript初心者のメモとして「JavaScriptで画像を表示する方法」を残します。
今まではホームページの作成にHTMLのみで画像を表示していましたが、せっかくJavaScriptを学習しているのでJavaScriptでも表示したいと思ってやってみました。

画像を表示する方法

1.HTMLで表示する方法

1枚だけ画像を画面に表示するためのHTMLです。(コピーすれば何枚でも表示できます)
imgタグにより画像を表示しています。
まずはhtmlの全体です。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="style.css">
  <title>HTMLで画像を表示する方法</title>
</head>
<body>
  <img src='images/image000.jpg'>
</body>
</html

次にimgタグだけみてみます。

  <img src='images/image000.jpg'>

imgタグのsrcの部分は画像がhtmlに対してどのこフォルダーに入っているかを表しています。
この場合だとimagesフォルダーの下にimage000.jpgという画像ファイルが入っています。
imagesフォルダーはhtmlと同じ階層(フォルダー内)にあります。
これでhtmlのみで画面上に画像を表示できます。

2.JavaScriptで表示する方法

ではこれからJavaScriptで画像を表示してみましょう

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="style.css">
  <title>HTMLで画像を表示する方法</title>
</head>
<body>
  <img src='images/image000.jpg'>
  <script src="main.js"></script>
</body>
</html
main.js
'use strict';
{
document.write('<img id="carousel__main" src="images/image000.jpg">');
}

上記がJavaScriptで画像を表示するためのコードです。

以下説明します。
HTMLにJavaScriptを読み込ませるためにスクリプトタグを追加しています。

<script src="main.js"></script>

画面に出力する時は以下のコードを使用します。

document.write
//画面に出力するコマンド

を使用して画面に出力することができます。
文字を表示する時は以下のように書きます。

main.js
'use strict';
{
document.write('JavaScriptで画像を表示する');
}

同じ容量で画像を出力してみます。

main.js
'use strict';
{
document.write('<img src="images/image000.jpg">');
}

以上の方法でJavaScriptで画像を画面に表示することができます。
ちなみに以下のように「'」ではなく「"」で書くと文字の区切る場所が異なり(青の文字部分)
エラーとなります。

main.js
'use strict';
{
document.write("<img src="images/image000.jpg">");
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Next.js】Base Pathを基礎からしっかり。

前書き

筆者がNext.jsを仕事で書くことになったので、1から勉強するためにアウトプット記事を書くことにしました。
基本的にはドキュメントを噛み砕いて、翻訳した記事です。間違っているところなどあれば、ご指摘していただけるとありがたいです?‍♂️

以下、本題です。

Base Path

どんな時に使うの?

Next.jsアプリケーションをドメインのサブパスの下にデプロイするには、basePath設定オプションを使うことができます

ちょっとわかりにくい。。。噛み砕いて解説してきます。

例えばexample.com/hogeみたいな感じでデプロイしたい場合、にBasePath設定をすることで解決してくれるということです。

具体的にどう設定するの?

example.com/hogeでデプロイしたい場合は、next.config.jsを下記のように設定してあげればOKです。

next.config.js
module.exports = {
  basePath: '/docs',
}

Links

他のページにリンクするには、next/linknext/routerを使えばbasePathを考慮して自動的に変換してくれます。例えば

export default function HomePage() {
  return (
    <>
      <Link href="/about">
        <a>About Page</a>
      </Link>
    </>
  )
}

このリンクコンポーネントは実際には下記のように出力されます。

<a href="/hoge/about">About Page</a>

ちゃんとhogeが追加されていることが確認できると思います。つまり、全てのリンクコンポーネントに対して、basePathを追加する必要はないということです。便利!

以上です。ではでは〜

参考記事

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

Next.jsで作ったWebアプリをデスクトップアプリ化した

はじめに

公式に例を参考にして、既に作ったWebアプリをデスクトップアプリ化してみました。公式チュートリアルで作ったものを例にして書いていきたいと思います。
とりあえずデスクトップアプリ化するのいう感じで、そのまま機能を全部使えるとは限らず、追加で色々な実装が必要になると思いますが、少しでも役立てたら嬉しいです。

参考:with-electron-typescript

今回は下の画像のようにsrcディレクトリにpagesやcomponentsディレクトリが入っている構成でやっていきたいと思います。
image.png

作成したリポジトリ:https://github.com/NozomuTsuruta/my-blog

導入

まず、以下のコマンドで必要なpackageをインストールします。

## npm
npm install electron-is-dev electron-next
npm install --save-dev electron electron-builder

## yarn 
yarn add electron-is-dev electron-next
yarn add -D electron electron-builder

Electron用のファイル作成

その後、以下のようにelectron-srcに3つのファイルを作成します。

image.png

electron-nextの型定義ファイル↓

electron-src/electron-next.d.ts
declare module "electron-next" {
  interface Directories {
    production: string;
    development: string;
  }

  export default function (
    directories: Directories | string,
    port?: number
  ): Promise<void>;
}

windowを開く、閉じる時の設定や処理↓

electron-src/index.ts
import { join } from "path";
import { format } from "url";
import { BrowserWindow, app, shell } from "electron";
import isDev from "electron-is-dev";
import prepareNext from "electron-next";

app.on("ready", async () => {
  await prepareNext(".");

  const mainWindow = new BrowserWindow({
    width: 1000,
    height: 800,
    webPreferences: {
      nodeIntegration: false,
      preload: join(__dirname, "preload.ts"),
    },
  });

  mainWindow.webContents.on("new-window", (event, url) => {
    event.preventDefault();
    shell.openExternal(url);
  });

  const url = isDev
    ? "http://localhost:8000/"
    : format({
        pathname: join(__dirname, "../out/index.html"),
        protocol: "file:",
        slashes: true,
      });

  mainWindow.loadURL(url);
});

app.on("window-all-closed", app.quit);

設定はお好みで↓

electron-src/tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "lib": ["dom", "es2017"],
    "module": "commonjs",
    "moduleResolution": "node",
    "noEmit": false,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "esnext",
    "outDir": "../main"
  },
  "exclude": ["node_modules"],
  "include": ["**/*.ts", "**/*.tsx", "**/*.js"]
}

package.jsonに追加

MyAppの部分はアプリ名です。

次にpackage.jsonに以下を追加

package.json
{
  ...
  "productName": "MyApp"
  "main": "main/index.js",
  "scripts": {
   ...
    "dev-electron": "tsc -p electron-src && electron .",
    "dist": "next build && next export && tsc -p electron-src && electron-builder"
  },
  ...
}

一応アプリアイコンはこんな書き方で変更できます↓

package.json
{
  ...
  "build": {
    "mac": {
      "icon": "./public/icons/icon.icns",
      "target": [
        "dmg"
      ]
    },
    "win": {
      "icon": "./public/icons/icon.ico",
      "target": "msi"
    }
  }
}

.gitignoreにmainとdistを追加しておきましょう

.gitignore
/main
/dist

作成

最後に以下のコマンドを実行します。Windowsのアプリを作りたい場合は後ろに--win --x64をつけます。

## npm
npm dist

## yarn
yarn dist

Finderなどからdmgファイル(windowsはフォルダーからexeファイル)を開くとインストールできます!
image.png

アプリケーションから開くとこんな感じになりました!
image.png

最後に

ここまで読んでいただきありがとうございます!私自身Electronに関しての知識がまだまだ浅いので詳しく説明できなかった部分が多いのでもう少し学習を進めていきたいと思います。少しでもお役に立てれば嬉しいです!

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

[Javascript]inputのチェックボックスでチェックを一つしかつけさせない

HTMLのinputで指定できるチェックボックス(checkbox)。

一つしか選択させたくない場合はまあラジオボタンにしておけという話かもしれないが、どうしてもチェックボックスでやりたい場合の方法をJQuery&JS両方で紹介。

oncheck.js
//Jqueryの場合
$(".inputClass").on("click", function(){
   $('.inputClass').prop('checked', false);
   $(this).prop('checked', true);
}); 

//JSの場合
const inputClass = document.getElementsByClassName("inputclass");
for(let i=0;i<inputClass.length;i++){
  inputClass[i].onclick = function(){
      for(let u=0;u<inputClass.length;u++){
        inputClass[u].checked = false;
        this.checked = true;
      }
   }
} 

入れ子で分かりづらいが、指定したクラスを持つすべてのinputにonclickを追加している。
onclickの先で、一度全てのinputのチェックを外し、onclickが発動したinputだけチェックを付けている。

HTMLはこんなの↓を想定

check3.html
<input type="checkbox" class="inputclass">1
<input type="checkbox" class="inputclass">2
<input type="checkbox" class="inputclass">3

余談だが、やはりinputはお堅いフォーム以外の場合での装飾にlabelを使わないといけないのがすごくめんどくさい。

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

JSで十字キーと他のキーを同時押しさせたい

はじめに

Webゲームを作っている時に、十字キーを二つ以上押しながら他のキーを押すと一部効かなかったので調べてみました。

調べ方

ただ単純に十字キーをいずれか二つ押しながら他のキーをポチポチする。

結果

以下が結果です。
image.png
これより、5つのパターンに分けられます。

  • どの時でも押せる
    • C,D,F,H,J,L,O,P,W
  • 「上」が含まれていると効かない
    • E,I,Q,T,Y
  • 「下」が含まれていると効かない
    • A,B,G,S
  • 「左」が含まれていると効かない
    • M,N,V,X,Z
  • 「右」が含まれていると効かない
    • K,R,U

なので、十字キーを押しながら押せるキーは限られてしまいます。
これには何か規則性があるのではないかと思い、適当にキーの色を分けてみました。
image.png
塗った感じ、上が含まれていると効かないキーは上の段、下が含まれていると効かないキーは下の段に固まっているぐらいで、特にこれといった規則性はなさそうでした。

まとめ

何か規則性があればよかったのですが、ないので十字キーを押しながら他のことをさせる場合は使用できるキーを限る必要がありそうです。
何か対策出来る案やライブラリがあれば教えてほしいです。

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