- 投稿日:2021-01-10T23:47:54+09:00
LINEボットでGoogle Alertを通知
Google Alertは、キーワードを登録しておくと、ウェブ上の新着コンテンツを知らせてくれるサービスです。私は、今まで、メールで受信していたのですが、せっかくコンテンツを通知してくれたのに、時間がたつと埋もれてしまっていました。
そこで、新着コンテンツが来たタイミングでLINEボットで通知すると同時に、データベースに登録して、あとでも参照できるようにしてみます。
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アラートを作成、と表示されているところにキーワードを入力します。
今回は、「ESP32」としてみました。オプションを表示となっている場合はクリックしてオプションを表示します。ここで、配信先として、自身のGmailアドレスではなく、「RSSフィード」を選択します。
最後に、アラートを作成 を押下します。そうすると、ESP32が追加され、無線のようなマークがでていますので、クリックします。
そうすると、RSSフィードが表示されました。
まだコンテンツの監視が始まったばかりで、コンテンツは1件もないです。ブラウザに表示されているこのURLを覚えておきます。データベースの準備
以下のようなスキーマのテーブルを作成しました。
データベース名:googlealert
テーブル名:items
コンテンツを格納します。テーブル名:members
LINEボットからコンテンツをメッセージ送信する先のユーザIDを格納します。LINEボットの作成
すみませんが、以下の投稿を参考にしてください。
LINEボット名は「Googleアラート」にしてみました。
定期的なコンテンツの取得
定期的なコンテンツ取得は、GoogleアラートのRSSフィードを参照することで行います。
また、これから立ち上げるNode.jsサーバへコンテンツをHTTP Postしています。RSSフィードの参照およびHTTP Postには以下のnpmモジュールを使っています。
rbren/rss-parser
https://github.com/rbren/rss-parsernode-fetch/node-fetch
https://github.com/node-fetch/node-fetchcron_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 installHTTPSである必要がありまして、SSL証明書をcertフォルダに置きます。フォルダ名は、app.jsを見ればわかります。
起動は以下の通りです。$ node app.jsRSSフィードされたコンテンツを受信する部分を抜粋します。
server/api/controllers/linebot-googlealert/index.jsexports.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.jsconst 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.jsvar 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.jsconst config = { channelAccessToken: '【LINEボットのチャネルアクセストークン(長期)】', channelSecret: '【LINEボットのチャネルシークレット】', };上記のシークレットを変更しないと、LINEボットのWebhook設定で、Webhook URLの検証が成功しないです。
コンテンツ取得とLINE通知を試してみる。
それでは、LINEボットを自身のスマホのLINEアプリから登録しましょう。
登録が完了すると、LINEボットがそれを認識し、LINEユーザのユーザIDをデータベースに登録します。以下の部分です。
server/api/controllers/linebot-googlealert/index.jsapp.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にも通知されます。
同時に、LINEアプリにも通知が届いているかと思います。
あとは、これを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.jsconst LIFF_ID = "【LINEのLIFF-ID】";画面はこんな感じです。
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.jsif( 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({}); }以上
- 投稿日:2021-01-10T22:51:09+09:00
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おわりに
まだしばらくバグ取りや安定化の時間が必要だと思います。
自然言語でプログラミングができたら楽だ、ソースも読みやすくなる、と思ってくださる方がおられましたら、こちらのライブラリを覚えておいていただくか、フィードバックや参加をいただけないかと思っています。
ぜひ今後ともよろしくお願いいたします。
- 投稿日:2021-01-10T22:10:42+09:00
【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 WorldHTML要素に値を追加する方法
サンプルコード<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>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>参考文献
要素の中身を変える!JavaScriptでinnerHTMLの使い方【初心者向け】
【JavaScript入門】innerHTMLでdivタグ内の要素を取得、設定する方法
- 投稿日:2021-01-10T21:47:33+09:00
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
でパラメータの更新処理を行います。なお、学習時の呼び出し順は次のように保証します。
- bind
- calc
- grad
- 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でも実行しやすいですしね。
- 投稿日:2021-01-10T19:25:50+09:00
初学者向け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へリクエスト。
③レスポンスが正しく返ってきたら、住所をフォームに自動入力し、マップを表示。実務未経験のためコードは荒いですが、動作は確認済みです。
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と通信するときに使うと思うので学習必須だと思っています。
今後も記事を更新して、より深みを出していきます。
(もしこの記事に誤りがありましたらご教授いただけると幸いです。)
- 投稿日:2021-01-10T18:31:59+09:00
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/
- 投稿日:2021-01-10T18:20:37+09:00
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);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);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); };以上
- 投稿日:2021-01-10T18:16:55+09:00
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);②(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);③(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);④( 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)));⑤ 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));⑥(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));以上
- 投稿日:2021-01-10T18:15:17+09:00
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)で生成します。
正解
tsファイル
employee.tsimport { 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.tsimport { 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 KEY
とUNIQ INDEX
の両方が設定されています。
UNIQUE INDEX
の方はいらないはずですが、消し方はわかりませんでした。(あってもパフォーマンスへの影響以外は無いと思われます)@PrimaryColumn('int', { name: 'employee_id', unique: false })としてみましたが結果は変わらず。
備考
TypeORM
0.2.25
MySQL5.7
で確認しました。
- 投稿日:2021-01-10T17:46:24+09:00
[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.jsimport 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 Topaxiosの導入
yarn add axios
axiosでfirestoreのデータを取得する
userShow.jsimport 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?
- 投稿日:2021-01-10T16:36:11+09:00
DB 操作まで JS で完結!Next.js × Prisma を CRUD アプリケーションでざっくり理解する。
何について書いた記事か?
- Next.js × Prisma を利用して CRUD アプリケーションを作成した ので、そのまとめ記事です.
- Next.js は React でアプリケーションを作成するためのフレームワークです.
- Prisma は Node.js のための ORM です.
- 各レイヤーの役割とソースコードを対応付けながら説明することで、『
Next.js
×Prisma
で作るとどんな実装になるのか?』 をざっくりと理解することを目指します.- 開発を始める前にこの記事で概要を掴む 的な使い方をしてくれると嬉しいです
こんな人にオススメ
- Next.js / Prisma に興味を持っていて、まずはこれらで実現できることを知りたい.
- React / Vue.js を書いていて、バックエンドも JS (TS) で書けたらいいなと思っている.
注意点
- サンプルアプリケーション開発のハンズオン的な説明は行っていません.
- 実際に手を動かす際には 開発に役立つチュートリアル集 などを参照してください.
- 自分は React, TypeScript の経験が浅いので、慣例 / ベストプラクティスに沿っていないソースがある と思います
- 逆に言えば、慣れていなくても楽しく、ストレスなく開発できました!
- 『ここはこう書いた方がいいよ!』といったご指摘があれば、コメントで教えていただけると嬉しいです
何を作ったか?
シンプルに CRUD できるだけのアプリケーションです.
- 題材は何でもよかったので、スターバックスのカスタマイズドリンクを CRUD する前提にしています.
ちゃんみおスペシャル を登録したかっただけ.
- 認証は入れていないので好きにデータ編集してもらってOKです.
- (不正利用対策は主目的でないので、常識の範囲内で抑えてくれると助かります )
- 性能: ローカル開発では気になりませんでしたが、デプロイすると DB との通信に結構時間がかかるようになってしまった (特に初回通信) ので、Vercel か AWS RDS で追加で設定すべき項目があるかもしれません.
Next.js × Prisma で作ったアプリケーションの構成
新しい技術を学ぶときの難しさの一つに 『色んな登場人物が出てきて関係性がわからない 』 ということがあると思っているので、最初に アプリケーションの主要な構成 を説明しておきます.
ただ、この説明だけでそれぞれについて深く理解することは難しいと思うので、まずは 『なるほど、こういうものがあるのね 』 と把握してもらえれば OK です.
WEB ページにアクセスされてから DB 操作までのフロー
- 図の が開発環境 / が本番環境です.
- 本番での差分にはオレンジの枠線を付けています.
- フロントエンドは 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 のコード例だけ紹介しておきます.Prisma の基本的な書き方を集中するため、エラーハンドリングは省略しています.
CREATE
CREATE するまでのフロー
CRAETE を行う React コンポーネント
pages/create.tsxexport 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); // 登録後のロジック ... };CRAETE を行う API Routes ( Prisma Client を利用 )
pages/api/beverages.tsconst 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); };READ
READ を行う API Routes ( Prisma Client を利用 )
pages/api/beverages.tsconst 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); };UPDATE
UPDATE / DELETE では、どのデータに対して操作するのかを特定するために、ファイル名を
[id].ts
として URL から情報を受け取っています.
ここでは、Next.js - Dynamic API Routes という仕組みを利用しています.UPDATE を行う API Routes ( Prisma Client を利用 )
pages/api/beverage/[id].tsconst 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); };DELETE
DELETE を行う API Routes ( Prisma Client を利用 )
pages/api/beverage/[id].tsconst 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 操作が簡単に書けていい ですね!
また、スキーマ定義を元に型定義が生成されるので、タイプセーフに書ける というのも大きなメリットです.
- より詳しい情報は、Prisma - Prisma Client などをチェックしてください.
Next.js × Prisma で開発するメリットは何か?
実際に開発して感じた
Next.js
×Prisma
のメリットについてまとめておきます.メリット : JS (TS) だけで完結するため、キャッチアップコストを抑えられる
- 当然ですが、フロントエンド / バックエンドが同一言語なので開発のキャッチアップコストを抑えることができます.
- また、今回開発した構成だと 従来の REST API と構成が近いため、そういった意味でも理解しやすい のではと思っています.
- React / Vue.js などのモダンフロントエンドに足を突っ込むとそれだけで結構なキャッチアップコストを支払うことになるので、『そこで学んだ技術でバックエンドも書きたい』というのは自然な発想で、それが実現できるという意味で 開発体験は非常によかった です.
メリット : 同一言語 / 同一ディレクトリのため共通化がしやすい
- 全てが JS で書かれることで、これまでフロントエンド / バックエンドで別々に管理されていた仕組みを簡単に共通化できるようになります.
- 例えば、今回のアプリケーションではバリデーション定義を共通化したので、以下に実装例を載せておきます.
バリデーション定義の共通化
BeverageFormSchema.ts
という Yup スキーマを定義して、それをフロントエンド / バックエンドそれぞれからimport
することで簡単にバリデーション定義を共通化することができました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(), });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} >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; }メリット : サーバー管理が楽になる
yarn dev
を実行して Next.js の開発用サーバーを立てれば SSR / ISR や API Routes もそこで実行されるため、フロントエンド / バックエンドで別々の開発用サーバーを管理しなくてよくなります.- 個人開発や小規模なアプリにとって、このコンパクトさは大きな魅力 だと思いました.
おわりに
いかがだったでしょうか.
Next.js × Prisma で開発をしてみて感じた良さが少しでも伝わっていれば嬉しいです.この記事で『興味を持ったよ』という方は、是非以下のチュートリアルなどを参考に実際に開発をしてみることをオススメします.
結局、手を動かしてみるのが一番理解できると思うので!以上、長い記事を読んでいただいてありがとうございました
開発に役立つチュートリアル集
- 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 の公式ページに追加されていた記事なので自分はこれには沿っていないのですが、上で説明したものと同じ技術要素でのチュートリアルです.
- プロジェクトの作成からデプロイまで丁寧に解説されていた ので、これに沿っていけば一通りの開発は順調に進められそうでした.
- 投稿日:2021-01-10T15:11:11+09:00
【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
pathname
:String
=> 現在のroute
を表示。/pages
内のpathを表示します。query
:Object
=> オブジェクトにパースされたクエリ文字列を表示します。asPath
:String
=> ブラウザに表示される(クエリを含む)pathを表示します。basePath
:String
=> 使える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> ) }オブジェクトの形で
pathname
とquery
を指定していることが確認できるかと思います。ここでは動的なルーティングを実現しています。router.replace
router.replace(url, as, options)それぞれの役目は
router.push
と同じです。
next/link
のreplace
のprop
と同様に、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> ) }基本的な使い方は以上です。ではでは〜
- 投稿日:2021-01-10T14:43:17+09:00
JavaScriptのスコープ内関数のホイスティングでハマった
ハマった内容
- ブロックスコープ内の関数はブロックスコープ外から呼び出せる。(ブロックスコープの仕様)
- 関数はホイスティングされるから、関数の定義より前に関数の呼び出しを書いても、問題なく呼び出せる。(ホイスティングの仕様)
と言うことは、ブロックスコープの外側で、ブロックスコープ内の関数定義より前に呼び出したら、呼び出せるのでは...?(馬鹿)
結論
ホイスティングは、スコープ内で巻き上げを行うものです。
スコープ外まで巻き上げないので、呼び出せません。
なので、Uncaught TypeError: fn is not a function
となります。ホイスティングとは
JavaScriptの仕様で、スコープの中で、変数や関数が先頭に巻き上げられることです。
具体例
Examplefn(); function fn() { console.log('hoge'); } /** * 出力:hoge * 理由:ホイスティングされるので、関数の定義より前に関数を呼び出していても、関数は問題なく実行される。 **/ブロックスコープとは
{...}
で括られたスコープのことです。
ブロックスコープ内の let / const はブロックスコープ外から参照できません。
function は参照できます。具体例
Example2{ let a = 0; function fn() { console.log('hoge'); } }Example2-1console.log(a); /** * 出力(エラー):Uncaught ReferenceError: a is not defined * 理由:ブロックスコープ内の let / const はブロックスコープ外から参照できない **/Example2-2fn(); /** * 出力:hoge * 理由:funtionは参照できる **/ハマったところ
Example3fn(); { let a = 0; function fn() { console.log('hoge'); } } /** * 出力(エラー):Uncaught TypeError: fn is not a function * 理由:ホイスティングはスコープ内が対象のため、ブロックスコープ定義より前からの呼び出しはできない **/まとめ
間違っていたら、コメントで教えていただけると嬉しいです。
- 投稿日:2021-01-10T14:38:43+09:00
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> </htmlmain.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">"); }
- 投稿日:2021-01-10T14:18:48+09:00
【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.jsmodule.exports = { basePath: '/docs', }Links
他のページにリンクするには、
next/link
やnext/router
を使えばbasePath
を考慮して自動的に変換してくれます。例えばexport default function HomePage() { return ( <> <Link href="/about"> <a>About Page</a> </Link> </> ) }このリンクコンポーネントは実際には下記のように出力されます。
<a href="/hoge/about">About Page</a>ちゃんと
hoge
が追加されていることが確認できると思います。つまり、全てのリンクコンポーネントに対して、basePath
を追加する必要はないということです。便利!以上です。ではでは〜
参考記事
- 投稿日:2021-01-10T11:53:25+09:00
Next.jsで作ったWebアプリをデスクトップアプリ化した
はじめに
公式に例を参考にして、既に作ったWebアプリをデスクトップアプリ化してみました。公式チュートリアルで作ったものを例にして書いていきたいと思います。
とりあえずデスクトップアプリ化するのいう感じで、そのまま機能を全部使えるとは限らず、追加で色々な実装が必要になると思いますが、少しでも役立てたら嬉しいです。今回は下の画像のようにsrcディレクトリにpagesやcomponentsディレクトリが入っている構成でやっていきたいと思います。
作成したリポジトリ: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-builderElectron用のファイル作成
その後、以下のように
electron-src
に3つのファイルを作成します。electron-nextの型定義ファイル↓
electron-src/electron-next.d.tsdeclare module "electron-next" { interface Directories { production: string; development: string; } export default function ( directories: Directories | string, port?: number ): Promise<void>; }windowを開く、閉じる時の設定や処理↓
electron-src/index.tsimport { 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 distFinderなどからdmgファイル(windowsはフォルダーからexeファイル)を開くとインストールできます!
最後に
ここまで読んでいただきありがとうございます!私自身Electronに関しての知識がまだまだ浅いので詳しく説明できなかった部分が多いのでもう少し学習を進めていきたいと思います。少しでもお役に立てれば嬉しいです!
- 投稿日:2021-01-10T02:04:07+09:00
[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を使わないといけないのがすごくめんどくさい。
- 投稿日:2021-01-10T01:32:00+09:00
JSで十字キーと他のキーを同時押しさせたい
はじめに
Webゲームを作っている時に、十字キーを二つ以上押しながら他のキーを押すと一部効かなかったので調べてみました。
調べ方
ただ単純に十字キーをいずれか二つ押しながら他のキーをポチポチする。
結果
- どの時でも押せる
- 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
なので、十字キーを押しながら押せるキーは限られてしまいます。
これには何か規則性があるのではないかと思い、適当にキーの色を分けてみました。
塗った感じ、上が含まれていると効かないキーは上の段、下が含まれていると効かないキーは下の段に固まっているぐらいで、特にこれといった規則性はなさそうでした。まとめ
何か規則性があればよかったのですが、ないので十字キーを押しながら他のことをさせる場合は使用できるキーを限る必要がありそうです。
何か対策出来る案やライブラリがあれば教えてほしいです。