- 投稿日:2020-05-24T23:47:30+09:00
javascriptで動画の明るさを調整できるchrome拡張機能を作ったから、作り方を1から解説してみる
初めに
adjusting the video bright
Youtubeやニコニコ動画の動画の明るさを調整できるchrome拡張機能です。地味に便利な拡張機能と思います。
自分なりの解釈で書いているので、間違っているところがあるかもしれないので参考程度に見ておいてください。
必要なファイル
- index.html (browser actionで開くときに必要)
- background.js (backgroundで必要)
- content.js (content_scriptで必要)
- manifest.json (chrome拡張機能の名前や説明、使うchrome APIなどを記述するときに使う 必須なファイル) 。
まとめる
まず、コードを書く前にやるべきことをまとめます。
1,ポップアップ画面はどのような画面にするか
明るさを調整するだけなので、シンプルでいいです。input type="range"で調整できるようにして、それを左に置き,
決定ボタンは右に置くようにします。
決定ボタンを押せば、動画が暗くなるようにします。汚くてすみません....
右上のiconをクリックすればポップアップが表示されて、rangeが左,ボタンが右 という感じです。
最低でもinputタグのrangeタイプとbuttonタグがあれば十分です。あとはお好みのデザインにしても構いません。2.必要なjavascriptファイル。メッセージパッシング
まずyoutubeの画面を暗くするには、cssであれこれやるのですが、それをするためにはDOM操作が必要になります。
ポップアップに使うHtmlファイルの外部jsファイルでやればいいかと思うとできません。DOM操作を行うためのJsファイルは決まってます。
manifest.jsonにあるcontents_scriptに指定したjsファイルです。ポップアップのhtmlファイルにcontent_scriptsに指定したjsファイルを外部jsファイルとしてdom操作をしたら、出来るのかというと,これもできません。
ポップアップするhtmlファイルをこの記事では拡張機能側と呼んでおきます。
拡張機能側からdom操作をすることはできません。じゃあどうすればいいのかというと、拡張機能側からcontent_scriptsのjsファイルに必要なデータを渡して、そのjsファイル(content.js)でdom操作を行うようにすればいいことです。
htmlだけじゃcontent_scriptsにデータを送信できないので、外部jsファイルでcontent_scriptsに送信します。
またデータを送信するにはmanifest.jsonのbackgroundに指定されたbackground.jsがchromeAPIを使い、送信していきます。流れをまとめると
1,拡張機能側で必要なinputの値を取って、決定ボタンを押す
2,外部jsファイル(background.js)がinputの値などのデータをcontent_scriptsに送信
3,content_scriptsはデータを受け取り、dom操作でページに反映する。
このような感じです。
この送信したり受け取ったりすることをメッセージパッシング(message passing)と言います。
コードを書いていく
1,DOM操作で、動画を暗くするやり方を知る。
まず,暗くするやり方を身につけましょう。Youtubeの動画の画面をデベロッパーツールで見ます。すると、videoタグでclass属性はvideo-streamなどが記載されていることが分かります。
では、そこに filter: brigtness(0.5); とデベロッパーツールの下の方にある、cssを入力できる場所に入力してみると画面が暗くなります。cssのfilterプロパティとは画像や動画を加工するために使われています。
特に画像に使われます。
MDNにbrightness以外のものもあるので見てください。つまり、content_scriptsで指定したjsファイル(content.js)で、videoタグのclass属性を取得さえすれば出来るということです。
let movie = document.getElementsByClassName("video-stream"); movie[0].style.filter = "brightness(0.5)";まず1行目でvideo-streamクラスを取得します。
2行目なのですが、なぜmovie[0]の[0]を付けたのかということを簡単に言いますと、特定するためです。
id属性は一つしか付与することはできないのに対し、class属性は複数のタグに同じクラス名を付与できます。movieだけだと、どのタグについてるvideo-streamクラスなのかわかりません。
なので、ここは一番最初のvideo-streamクラスにfilterプロパティをつけたいので、movie[0]としています。<div class="video-stream">0</div> //movie[0] <div class="video-stream">1</div> //movie[1]一つ目のタグを取りたいなら[0]をつけ、2つ目のタグをとりたいなら[1]をつけるようにする。
なお、id属性をとるのなら、[0]などについては必要ありません。2,manifest.jsonを作る
まずサンプルをここに載せて置きます。
manifest.json{ "manifest_version": 2, "name": "", "description": "", "version": "1.0", "browser_action": { "default_icon" : "", "default_title": "", "default_popup": "" }, "icons":{ "16":"16x16....", " } "permissions" : [ ], "content_scripts": [{ "matches": ["<all_urls>"], "js": [""] }], "background":{ "scripts": [""], "persistent": false } }masnifest.jsonの書き方
manifest.jsonにはこういう感じで書いていきます。
まず、"manifest_version" : 2とありますが、manifestのバージョンで、現在バージョンが2であるためです。これは絶対にこのまま書いておきましょうnameとは拡張機能の名前です。
descriptionとは拡張機能の説明です。どのような拡張機能か書きましょう。
versionとは拡張機能のバージョンを指します。なにか変えたりしてアップデートを行った場合、versionも変えましょう。作成した拡張機能をアップデートをして、再びウェブストアにアップロードするときversionを変えてなかったら、エラーガ起こります。
続いてbrowser_actionについて
default_iconは拡張機能のアイコンです。
38x38 の大きさの画像を作成して、指定してあげてください。default_titleは右上にある拡張機能のアイコンにマウスオーバー(マウスを乗せる)すると出てくる名前です。
default_popupに関しては,htmlファイルを指定します。
指定した場合、アイコンをクリックすると、ポップアップが出てきます。
指定しなかった場合,何も出てきません。iconsに指定された画像はウェブストアの時に使います
16x16 48x48 128x128の大きさを指定してあげてくださいpermissionsではchrome APIを使いたいときに記述します。
permissionsに書かなくても使えるchromeAPIはあります。content_scriptsはdom操作をするときに使います。
machesには正規表現で書き、拡張機能を使う対象のサイトを書きます。
jsでは動作させるスクリプトを書きます。
content_scriptsで、今見ているタブ(サイト)をDOM操作で色々と書き換えることができます。background: バックグランドページとイベントページがあります。
今はイベントページが推奨されています。
scripts で バックグランドで動作するjsファイルを指定します。
persistentはfalseにすると、イベントページとなり何も書かなかったらバックグランドページとなります。
バックグラウンドページは裏側ではずっと動いているのに対して、イベントページでは必要なときに動くのでpersistent: falseは書いておきましょう。ポップアップ画面を作る
次にポップアップ画面をデザインしていきます。
これは好きなようにして構いませんが、最低でも<input type="range"> <button></button>この二つは必要です。
3,chrome APIをリファレンスを読みながら作っていく。
基本的に chrome api を読んでいきながら作ってみましょう。まず,どんなapiを使うのかまとめると
chrome tabs api
chrome runtime apiこれらを使っていきます。
tabs APIで送信して、runtime APIで受け取るようにします。
なおtabs APIはbackgroundで動くjavascriptでないと動作しないので、ポップアップするHtmlファイルの外部jsにbackground.jsを使います。つまり
1,htmlのinputタグの値をbackground.jsで取得し、tabsAPIを使って送信2,content_scriptsのjsで,background.jsが送信したデータをruntime APIで受け取る
3,受け取ったデータを使いDOM操作をやる。
background.jsを書いていこう!
background.jslet btn = document.getElementById('btn'); //button を取得 let col = document.getElementById('elem'); // input type=range を取得ボタンをクリックしたら、送信したいので
background.jsbtn.addEventListener('click',function(){ //ここにapiを記述 }このように記述します。
まずchrome tabs apiに行きます。tabsAPIで使いたいメソッドは、
- query()
- sendMessage()
この二つのメソッドとなります。
まずqueryメソッドについて学んでいこうと思います。
chrome.tabs.query(object queryInfo, function callback)
このようなコードとなっています。何が何だかわからないと思いますが見ていきましょう。
queryメソッドの引数にはobject queryInfo, とあります。最後にコンマ( , )があるので、第一引数はここまでということが分かります。まずobjectとありますが、これは型を示しています。
javascriptでオブジェクトを書くときは,sample.jslet obj = { "***": *** }と書くように、このobject queryInfoも{}で囲んであげて、
その中に ○○○:○○○と書いていきます。sample.jschrome.tabs.query({}, function callback) ↓ chrome.tabs.query({○○○:○○○, ○○○:○○○}, function callback)このような感じになります。この{}の中にjsonと同じように書いていきます。
公式サイトを見ますが、
active pinned audibleとずらっと書いてあります。その左にbooleanと書いてありますが、これが何を示しているのかというと、簡単に言えば設定です。
booleanというとtrueやfalseということなので、
{active: true} また{active: false}このような感じで書けということです。ここで使うオプションはactiveとcurrentWindowの二つのオプションです。
ちなみにどっちもbooleanです。activeはタブがウィンドウでアクティブかどうか。とグーグル翻訳で出ています。
つまり今このタブを開いているかどうかです。
trueにしましょう。currentWindowはタブが現在のウィンドウにあるかどうかです。
つまり、今見ているサイトかどうかです。これもtrueにしましょう。この二つのオプションをtrueにすることで、開いているサイトで尚且つ、今見ているサイトの情報を取得することになります。
sample.jschrome.tabs.query({active: true, currentWindow: true }, () => { })object queryInfoが第一引数に対して、第二引数はfunction callbackです。
引数の中に関数があるため、第二引数はコールバック関数であることは一目瞭然です。コールバック関数の引数には,object queryInfoで指定されたタブ(サイト)の情報があり、それをsendMessageメソッドに渡します。
sample.jschrome.tabs.sendMessage(integer tabId, any message, object options, funct ion responseCallback)第一引数には、見ているサイトのtabIdを書かないといけないので、queryメソッドのコールバック関数にサイトの情報 (tabId)があるので、使います。
sample.jschrome.tabs.query({active: true, currentWindow: true},tab => { chrome.tabs.sendMessage(tab[0].id, any message, object options, fu nction responseCallback) })tab[0].idでtabIdを取得できます。
第二引数には送信したいデータを指定します。
公式サイトには送信するメッセージ。このメッセージは、JSONで送信可能なオブジェクトである必要がありますと書いています。
つまりjson形式で書いてくださいということです。また送信したいデータはinput type="range"の値です。
sample.jslet col = document.getElementById('elem'); // input tag chrome.tabs.query({active: true, currentWindow: true},tab => { chrome.tabs.sendMessage(tab[0].id, { dark : Number(col.value)}) }){dark : Number(col.value)}と第二引数に書きました。
Numberメソッドで、引数にあるinput range の値を数値に変えています。第三引数からに関しては必要ないと思っています。
まず第一、このinput rangeの値さえcontent_scriptsに渡せばいいのですから、一方的なんですよね。これをまとめたコードがこちらです。
background.jslet btn = document.getElementById('btn'); let col = document.getElementById('elem'); btn.addEventListener('click',function(){ chrome.tabs.query({active: true, currentWindow: true},tab => { chrome.tabs.sendMessage(tab[0].id,{ dark : Number(col.value) }) }) })これでボタンをクリックすればapiが実行され、content_scriptsに送信されます。
次はcontent.jsで送信されたデータを受け取り、dom操作で反映させます。
content.jsを書く
受け取る方法は簡単で,
runtime api のchrome.runtime.onMessage.addListener()を使うだけです。content.jschrome.runtime.onMessage.addListener(function callback)引数の中に関数functionがあるのでコールバック関数となります。
コールバック関数の引数には,第一引数にmessage, 第二引数にsender, 第三引数にsendResponseがあります。
content.jschrome.runtime.onMessage.addListener( function(message, sender, sendResponse){ } )受け取ったデータをDOM操作するというシンプルなことですから、第一引数のmessageだけで、問題ないです。
messageには送信したデータが入っています。あとは、最初に学んだDOM操作を関数の中に書いてあげます。
content.jschrome.runtime.onMessage.addListener( function(message, sender, sendResponse){ let movie = document.getElementsByClassName('video-stream'); movie[0].style.filter = `brightness(${message.dark})` } )
brightness(${message.dark})
はテンプレートリテラルを使ってます。バッククォートで囲んであげてください。作った拡張機能を読み込んでみよう
これで拡張機能は作れたので読み込んでみましょう。
1, まずブラウザを開き,右上の3つの点をクリック
2, その他のツールをクリック出てきた、拡張機能をクリック
3, パッケージ化されていない拡張機能を読み込むをクリックし、フォルダを選択
4,完了です。ページに戻り右上に見ると追加されているのが分かります。
エラーの場合は、manifest.jsonにちょっとおかしいところがあります。なおしましょう。ウェブストアにアップロードしよう。
実際にアップロードするのは、この記事で作った拡張機能ではなく、オリジナルの拡張機能でお願いします。
1.chromeウェブストアに行く。
2,右上にある歯車マークを押し,デベロッパーダッシュモードを選択
3,1ドル(だいたい100円)を払う。
4,作ったフォルダを圧縮してzipにしてから、アップロード。
5,ダッシュボードに行き、英語で拡張機能の説明を書き、完了したら審査に出す。自分は英語が書けないので、DeepLを使い書きました。これでアップ完了です。自分はアップしてから五日ほどで審査が終わり、ストアに出せるようになりました。最初は検索しても出てきませんでしたが、2日ほど待つと後ろの方ですが検索に出てくるようになりました。
またアップデートしたものを、ウェブストアに出すとき、審査されますが自分は1日で終わりました。終わりに
初めて拡張機能を作ってみましたが、chromeAPIがちょっと難しかったなと思います。
adjusting the video brightという拡張機能をできれば試してほしいと思います。
UIが崩れてるかもしれないので、もし崩れてたら教えてほしいです。
お願いします!
- 投稿日:2020-05-24T23:42:08+09:00
Rails6.0でselectboxの複数選択をおしゃれにするJavaScriptライブラリ「select2」の導入方法
はじめに
皆様、こんにちは!
佐久間まゆちゃんのプロデューサーの@hiroki_tanakaです。先日、Rails6.0系でselectboxの複数選択をおしゃれにするプラグインのselect2を導入することがありました。
その際にRails5系までの導入方法との違いに少しハマったので、調べたことをまとめました。利用環境
- Ruby 2.6.6
- Rails 6.0.2
select2とは
select2とはHTMLのselectboxのデザインをおしゃれにするJavaScriptのライブラリです。
公式サイトはこちらです。→select2
単一選択のselectboxのUIだけでなく、複数選択のselectboxも簡単に実装する事ができるプラグインです。公式サイト上のサンプル
Rails6.0での導入方法
Rails5系でのselect2の導入はGemを使用しての導入となっていましたが、Rails6.0からはGemではなくWebpacker使用するようになりました。
1つずつ手順を紹介したいと思います。※Rails5でselect2の導入は下記のページが非常に詳細に説明しています。
→【Rails5】Select2で複数選択可能なセレクトボックスを作る1. yarnでselect2に必要なライブラリ導入
下記のコマンドを実行して、Railsアプリケーションにselect2を導入します。
この時、select2だけではなくselect2の導入に必要なjQuery(及びpopper.js)とselect2のUI部分に当たるbootstrapも併せて導入します。$ yarn add jquery $ yarn add popper.js $ yarn add bootstrap $ yarn add select22. Rails上でjQuery・popper.jsを使用できるように設定
Railsアプリケーションの
webpack/environment.js
にjQueryをRailsのどのファイルからも呼び出せるように設定します。config/webpack/environment.jsconst { environment } = require('@rails/webpacker') // jQueryとBootstapのJSを使えるように const webpack = require('webpack') environment.plugins.prepend( 'Provide', new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', Popper: 'popper.js' }) ) module.exports = environmentまた、
packs/application.js
のマニュフェストにJQueryを追加します。app/javascript/packs/application.jsrequire("jquery")3. Rails上でbootstrapを使用できるように設定。
下記の2つの設定を行い、Railsアプリケーションでbootstrapを使用できるように設定します。
app/javascript/packs/application.jsimport 'bootstrap'; import '../stylesheets/application';app/javascript/stylesheets/application.scss@import '~bootstrap/scss/bootstrap';4. erbファイルでwebpackerを使用するように設定。
Railsアプリケーションの全てのViewファイルでwebpackerに導入したbootstrap・jQueryが使用できるように大元の
layouts/application.html.erb
に設定します。app/views/layouts/application.html.erb<head> <%= stylesheet_pack_tag "application", media: "all" %> <%= javascript_pack_tag 'application' %> </head>5. selectboxをerb上に定義し、select2を使用するようにJSファイルに記載。
アプリケーションのerb上に複数選択を行うselectboxを定義します。
この時、select_tagはmultiple: true
とoption設定すれば、複数選択可能なselectboxとなります。そして、該当のselectboxがselect2を使用するようにerbファイルに対応するJSファイルに記載します。
app/views/test.html.erb<%= javascript_pack_tag 'test' %> <%= select_tag('animal', options_for_select([['いぬ', 'dog'], ['ねこ', 'cat'], ['とり', 'bird'], ['うし', 'cow'], ['へび', 'snake']]), class: "form-control", multiple: true) %>app/javascript/packs/test.js$(function () { $('#animal').select2(); });6. 完成!!
おまけ:select2のoption利用
select2には様々なoptionが用意されています。→select2 Options
これらのoptionはjQueryで設定することが可能です。
下記はoption設定の一例です。app/javascript/packs/test.js$(function () { $('#animal').select2({ width: 'resolve' // 幅をページサイズに併せて動的に変更する。 theme: "classic" // クラシックUIに変更する。 debug: true // ブラウザのコンソールにデバッグメッセージを出力する。 }); });おわりに
yarnを使用することでselect2の導入が更に簡単になったように感じます。
ただ、select2の内部挙動に関してはわからないことが多いので理解を深めたいです。
- 投稿日:2020-05-24T23:36:46+09:00
GoogleMapを使ってサイクリングに出かけよう
タイトルから、何の技術の話かわからないですね。
GoogleMapにナビゲーション機能ってありますよね、それをサイクリングでナビゲートしてもらおうというものです。
サイクリング中は、手が使えないので、音声ナビがあると便利です。
ですが、ポケストップを探すたびに、立ち止まってスマホを見るのは手間ですし、軽快ではありません。そこで、(ポケストップに限りませんが)サイクリング前に、途中のチェックポイントを複数覚えておいて、チェックポイントまでの道のりをナビゲートしてもらって、到着したら、ポケストップのアイテムをもらったり、コンビニで休憩したのち、次のチェックポイントを目指す、というものです。
原理の説明:GoogleMapの機能
GoogleMapは、外部のアプリやブラウザから起動させることができます。
その際に、パラメータの指定によって、場所の表示だけでなく、出発地点と目的地を指定して、ナビゲーションを開始させることもできます。ですので、まずはブラウザから、サイクリングで回りたいチェックポイントのリストを作って、GoogleMapを起動して次のチェックポイントまでナビゲーションしてもらいます。
チェックポイントまで到着したら、またブラウザを立ち上げて、今度は次のチェックポイントを指定してGoogleMapを起動させる、これを最後の目的地まで繰り返すわけです。GoogleMapのナビゲーション機能
ナビゲーション中は、以下のことを適宜音声で教えてくれます。
・予定通りの道を進んでいるかどうか
・次の曲がり角まで何メートルか
・今曲がるべき曲がり角か
・予定の道を外れたか
・目的地の近くに来たか
・目的地に着いたか上記は、ナビを開始すると、設定が選べるようになり、「詳しい音声案内」のスイッチをOnにした場合です。Offの場合はもうちょっと少ない気がします。
もろもろGitHubに上げておきました。
poruruba/orientation_navigator
https://github.com/poruruba/orientation_navigator画面説明
ブラウザを起動するとこんな感じの画面が表示されます。
まずは、チェックポイントを追加します。タブ「チェックポイント」を選択し、チェックポイントを追加します。
GoogleMapがはめ込まれて表示されるので、出発地点を選択します。
さらに、同じように次のチェックポイントを追加します。
経由地っていうチェックボックスがあります。通常は次のチェックポイントに到着するとGoogleMapのナビゲーションが終わってしまうのですが、経由地は次のチェックポイントまでの途中の経由地であって、経由地を通過しただけでは、GoogleMapのナビゲーションは終わらないようにしています。
オリエンテーションタブを選択すると、マーキングされているのがわかります。
これで準備完了です。
さっそく、「オリエンテーション開始」ボタンを押下してみましょう。そうすると、こんな感じでGoogleMapが立ち上がり、ナビゲーション開始待ちとなります。
ちなみに、PCのChromeブラウザからの画面ですが、Androidから使うと、ブラウザのGoogleMapか、ネイティブのGoogleMapアプリ、どちらを起動するかの選択肢が出てきます。もちろん、ネイティブのGoogleMapアプリの方がナビゲーションとしては使い勝手が良いです。あとは、開始してしまえば、いつものナビゲーションが始まります。
イヤホンで、GoogleMusic(Youtube Music)でも聞きながら、サイクリングしましょう。チェックポイントに到着したら、もう一度ブラウザに戻りましょう。
「チェックポイントに到着しましたか?」ボタンを押下すると、次のチェックポイントに出発のボタンに代わりますので、押下すると、またGoogleMapが立ち上がります。今度は、1つ目のチェックポイントから、2つ目のチェックポイントへのナビゲーションです。
おおよそ、イメージはつかめましたでしょうか?
マイスポット機能
毎度毎度、場所を選択するのはめんどうです。特に家の周りはいつものコースを決めていますが、毎度ポケストップを指定するのは面倒です。
そこで、あらかじめよくいくスポットをマイスポット機能として登録しておけば、それを選択するだけで、チェックポイントに追加されるようになります。マイスポットのタブを選択して、登録します。
そうすると、こんな感じで、チェックポイント登録する際に、マイスポットから選択することができます。
サーバ同期
実は、チェックポイントを追加したり、マイスポットを追加したりしたら、サーバ側にデータを保持するようにしています。ですので、ブラウザを立ち上げなおしても、以前の状態が復元されるようにしています。また、ナビゲーション中にチェックポイントに到達したりした時もサーバ側に同期するようにしています。
とはいってもクライアント・サーバいずれもかなり手抜きしています。
クライアント側は、Vueのwatchを使って、対象のデータが変更されたら、サーバに一括アップロードしているだけです。
取得も、ブラウザでの起動時だけです。GoogleMap起動のURL生成
大事なところをピックアップしました。
travelmode、origin、destination、waypointsを指定しているのがわかります。travelmode
歩きの移動か、車の移動か、電車の移動かを指定します。自転車がありますが、日本では使えないようです。origin
出発地点です。ブラウザで設定したチェックポイントに相当します。経由地ではありません。destination
これもブラウザで設定したチェックポイントですが、originのチェックポイントの次のチェックポイントです。waypoints
これが経由地です。start.js// GoogleMap起動のパラメータを生成 var params = ""; var origin = this.checkpoints[this.origin_index]; params += "&travelmode=" + this.travelmode; params += "&origin=" + encodeURIComponent(origin.lat + ',' + origin.lng); if( destination_index <= (this.checkpoints.length - 1)){ var destination = this.checkpoints[destination_index]; params += "&destination=" + encodeURIComponent(destination.lat + ',' + destination.lng); } if( (this.origin_index + 1) < destination_index ){ var waypoints = ""; for( var i = (this.origin_index + 1) ; i < destination_index ; i++ ){ if( i != (this.origin_index + 1) ) waypoints += '|'; waypoints += this.checkpoints[i].lat + ',' + this.checkpoints[i].lng; } params += "&waypoints=" + encodeURIComponent(waypoints); } var href = 'https://www.google.com/maps/dir/?api=1' + params; console.log(href); this.destination_completed = false; // GoogleMapを起動 window.open(href, '_blank');詳しくは以下を参照してください。
GoolgeMap Developers Guide Univarsal cross-platform syntax
https://developers.google.com/maps/documentation/urls/guide?hl=ja#directions-actionソース一式
Javascriptのソースです。
start.js'use strict'; //var vConsole = new VConsole(); const default_lat = 35.465878; const default_lng = 139.622329; const base_url = "http://localhost:10080"; var vue_options = { el: "#top", data: { progress_title: '', // for progress-dialog origin_index: 0, // 出発のインデックス destination_completed : true, // チェックポイントに到着したかどうか checkpoints: [], // チェックポイントのリスト map_markers: [], // Map1に配置のマーカ myspots: [], // マイスポットのリスト dialog_params: {}, // モーダルダイアログの入出力パラメタ map2_markers: [], // Map2に配置のマーカ default_latlng: new google.maps.LatLng(default_lat, default_lng), // デフォルトのロケーション(現在地に上書き) travelmode: 'walking', // GoogleMapに指定するtravelmode }, computed: { // ボタンに表示するテキスト orientation_text: function(){ if( !this.destination_completed ) return 'チェックポイントに到着しましたか?'; if( this.origin_index == 0 ) return 'オリエンテーション開始'; else if( this.origin_index >= (this.checkpoints.length - 1) ) return '最終目的地に到着しました。'; else return '次のチェックポイントに出発'; } }, watch: { checkpoints: function(){ // Mapに配置のマーカを再設定 for( var i = 0 ; i < this.map_markers.length; i++ ) this.map_markers[i].setMap(null); this.map_markers = []; for( var i = 0 ; i < this.checkpoints.length ; i++ ){ var latlng = new google.maps.LatLng(this.checkpoints[i].lat, this.checkpoints[i].lng); var mopts = { position: latlng, map: this.map, label: String(i + 1) }; var marker = new google.maps.Marker(mopts); this.map_markers.push(marker); } try{ // サーバに同期 update_data('checkpoints', this.checkpoints); }catch(error){ this.toast_show("サーバにデータをアップロードできませんでした。"); }; }, myspots: function(){ try{ // サーバに同期 update_data('myspots', this.myspots); }catch(error){ this.toast_show("サーバにデータをアップロードできませんでした。"); }; }, origin_index: function(){ try{ // サーバに同期 update_data('orientation', { origin_index: this.origin_index, destination_completed: this.destination_completed }); }catch(error){ this.toast_show("サーバにデータをアップロードできませんでした。"); }; }, destination_completed: function(){ try{ // サーバに同期 update_data('orientation', { origin_index: this.origin_index, destination_completed: this.destination_completed }); }catch(error){ this.toast_show("サーバにデータをアップロードできませんでした。"); }; }, }, methods: { // デフォルトのロケーションに移動 map_goto_current_location: function(){ var latlng = this.default_latlng; this.map.setCenter(latlng); }, // オリエンテーションタブ選択時にデフォルトのロケーションまたは出発位置に移動 orientation_update_view: function(){ var latlng = this.default_latlng; if( this.checkpoints.length > 0 ) latlng = new google.maps.LatLng(this.checkpoints[this.origin_index].lat, this.checkpoints[this.origin_index].lng); this.map.setCenter(latlng); }, // オリエンテーションを指定位置からリスタート orientation_restart: function(index){ if( index < 0 ) if( !window.confirm('本当に最初から初めてもいいですか?') ) return; this.origin_index = (index < 0) ? 0 : index; this.destination_completed = true; this.orientation_next(); }, // 次の目的地(経由地を除く)を取得 get_next_destination: function(){ var destination_index = this.origin_index + 1; for( ; destination_index < this.checkpoints.length ; destination_index++ ) if( !this.checkpoints[destination_index].waypoint ) break; if( destination_index >= this.checkpoints.length ) destination_index = this.checkpoints.length - 1; return destination_index; }, // 次へのボタンを押下 orientation_next: function(){ if( this.checkpoints.length == 0 ){ alert('チェックポイントを追加してください。'); return; }else if( this.checkpoints.length == 1 ){ alert('次のチェックポイントを追加してください。'); return; } if( this.origin_index >= (this.checkpoints.length - 1) ){ alert('すでに目的地に到達しています。'); return; } var destination_index = this.get_next_destination(); if( (destination_index - (this.origin_index + 1)) > 9){ alert('経由地の数が多すぎます。(9以下)'); return; } if( !this.destination_completed ){ // チェックポイントに到達 this.origin_index = destination_index; this.destination_completed = true; // 目的位置に到達 if( destination_index >= (this.checkpoints.length - 1) ) this.dialog_open('#orientation_complete_dialog'); return; } // GoogleMap起動のパラメータを生成 var params = ""; var origin = this.checkpoints[this.origin_index]; params += "&travelmode=" + this.travelmode; params += "&origin=" + encodeURIComponent(origin.lat + ',' + origin.lng); if( destination_index <= (this.checkpoints.length - 1)){ var destination = this.checkpoints[destination_index]; params += "&destination=" + encodeURIComponent(destination.lat + ',' + destination.lng); } if( (this.origin_index + 1) < destination_index ){ var waypoints = ""; for( var i = (this.origin_index + 1) ; i < destination_index ; i++ ){ if( i != (this.origin_index + 1) ) waypoints += '|'; waypoints += this.checkpoints[i].lat + ',' + this.checkpoints[i].lng; } params += "&waypoints=" + encodeURIComponent(waypoints); } var href = 'https://www.google.com/maps/dir/?api=1' + params; console.log(href); this.destination_completed = false; // GoogleMapを起動 window.open(href, '_blank'); }, // Map2のマーカをクリアし、指定場所に移動 map2_cleanup: function(latlng){ for( var i = 0 ; i < this.map2_markers.length; i++ ) this.map2_markers[i].setMap(null); this.map2_markers = []; if( this.map2_default_marker ){ this.map2_default_marker.setMap(null); this.map2_default_marker = null; } this.map2.setCenter(latlng); }, // モーダルダイアログの結果処理 dialog_submit: function(){ if( this.dialog_params.title == 'マイスポットの追加' ){ var location = this.map2.getCenter(); var name = this.dialog_params.name; this.myspots.push({ name, lat: location.lat(), lng: location.lng() }); }else if( this.dialog_params.title == 'チェックポイントの追加' ){ var location = this.map2.getCenter(); var name = this.dialog_params.name; this.checkpoints.push({ name, lat: location.lat(), lng: location.lng() }); }else if( this.dialog_params.title == 'マイスポットの変更'){ var location = this.map2.getCenter(); this.myspots[this.dialog_params.index].lat = location.lat(); this.myspots[this.dialog_params.index].lng = location.lng(); }else if( this.dialog_params.title == 'チェックポイントの変更' ){ var location = this.map2.getCenter(); this.checkpoints[this.dialog_params.index].lat = location.lat(); this.checkpoints[this.dialog_params.index].lng = location.lng(); } this.dialog_close('#select_location_dialog'); }, // マイスポットの追加(地図から)のためのモーダルダイアログ表示 do_myspot_append: function(){ this.map2_cleanup(this.default_latlng); this.map2_default_marker = new google.maps.Marker({ position: this.default_latlng, map: this.map2, }); this.dialog_params = { title: 'マイスポットの追加', is_input_name: true, is_input_submit: true, }; this.dialog_open('#select_location_dialog'); }, // マイスポットの削除 do_myspot_delete: function(index){ if( !window.confirm('本当に削除していいですか?') ) return; Vue.delete(this.myspots, index); }, // マイスポットの名前変更 do_myspot_change_name: function(index){ var name = window.prompt('新しい名前', this.myspots[index].name); if( !name ) return; this.myspots[index].name = name; }, // マイスポットのロケーション変更 do_myspot_change_location: function(index){ var latlng = new google.maps.LatLng(this.myspots[index].lat, this.myspots[index].lng); this.map2_cleanup(latlng); this.map2_default_marker = new google.maps.Marker({ position: latlng, map: this.map2, }); this.dialog_params = { title: 'マイスポットの変更', index: index, is_input_submit: true, }; this.dialog_open('#select_location_dialog'); }, // チェックポイントリストのリセット do_checkpoints_reset: function(){ if( !window.confirm('本当にリセットしていいですか?') ) return; this.origin_index = 0; this.destination_completed = true; this.checkpoints = []; }, // チェックポイントの追加(マイスポットから)のためのモーダルダイアログ表示 do_checkpoint_append_myspot: function(){ if( this.myspots.length == 0 ){ alert('マイスポットが登録されていません。'); return; } this.map2_cleanup(this.default_latlng); this.dialog_params = { title: 'チェックポイントの追加(マイスポット)', }; var _this = this; for( var i = 0 ; i < this.myspots.length ; i++ ){ var mopts = { position: new google.maps.LatLng(this.myspots[i].lat, this.myspots[i].lng), map: this.map2, }; var marker = new google.maps.Marker(mopts); this.map2_markers.push(marker); marker.addListener('click', function(e){ for( var i = 0 ; i < _this.map2_markers.length ; i++ ){ if( _this.map2_markers[i] == this ){ _this.checkpoints.push(_this.myspots[i]); _this.dialog_close('#select_location_dialog'); return; } } }); } this.dialog_open('#select_location_dialog'); }, // チェックポイント追加(地図から)のためのモーダルダイアログ表示 do_checkpoint_append: function(){ this.map2_cleanup(this.default_latlng); this.map2_default_marker = new google.maps.Marker({ position: this.default_latlng, map: this.map2, }); this.dialog_params = { title: 'チェックポイントの追加', is_input_name: true, is_input_submit: true, name : (this.checkpoints.length == 0) ? '現在地' : '', }; this.dialog_open('#select_location_dialog'); }, // チェックポイントの削除 do_checkpoint_delete: function(index){ if( !window.confirm('本当に削除していいですか?') ) return; Vue.delete(this.checkpoints, index); }, // チェックポイントの名前変更 do_checkpoint_change_name: function(index){ var name = window.prompt('新しい名前', this.checkpoints[index].name); if( !name ) return; this.checkpoints[index].name = name; }, // チェックポイントのロケーション変更 do_checkpoint_change_location: function(index){ var latlng = new google.maps.LatLng(this.checkpoints[index].lat, this.checkpoints[index].lng); this.map2_cleanup(latlng); this.map2_default_marker = new google.maps.Marker({ position: latlng, map: this.map2, }); this.dialog_params = { title: 'チェックポイントの変更', index: index, is_input_submit: true, }; this.dialog_open('#select_location_dialog'); }, // チェックポイントの順番変更 do_checkpoint_change_index: function(index, event){ var newIndex = event.target.selectedIndex; var temp = this.checkpoints[index]; Vue.set(this.checkpoints, index, this.checkpoints[newIndex]); Vue.set(this.checkpoints, newIndex, temp); }, }, created: function(){ }, mounted: function(){ proc_load(); // 現在地情報の取得 navigator.geolocation.getCurrentPosition((pos) =>{ this.default_latlng = new google.maps.LatLng(pos.coords.latitude, pos.coords.longitude); this.map_goto_current_location(); }, (error) =>{ this.toast_show("現在地を取得できませんでした。"); }); // Mapの生成 var myOptions = { zoom: 15, center: this.default_latlng, mapTypeId: google.maps.MapTypeId.ROADMAP, mapTypeControl: false, streetViewControl: false, }; var canvas = $('#map_canvas')[0]; this.map = new google.maps.Map(canvas, myOptions); // Map2(モーダルダイアログ用)の生成 var canvas2 = $('#map_canvas2')[0]; this.map2 = new google.maps.Map(canvas2, myOptions); google.maps.event.addListener(this.map2, 'center_changed', () =>{ if( !this.map2_default_marker ) return; var location = this.map2.getCenter(); this.map2_default_marker.setPosition(location); }); // サーバ保持データの取得 get_data('myspots') .then(data =>{ this.myspots = data; return get_data('checkpoints'); }) .then(data => { this.checkpoints = data; return get_data('orientation'); }) .then(data => { if( data.origin_index != undefined ) this.origin_index = data.origin_index; if( data.destination_completed != undefined ) this.destination_completed = data.destination_completed; }) .catch(error =>{ this.toast_show("サーバからデータを取得できませんでした。"); }); } }; 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(); }); } async function get_data(type){ return do_post(base_url + '/get-data', { type: type }) .then(json =>{ if( json.status != 'OK' ) throw "post failed"; return json.result.data; }); } async function update_data(type, data){ var body = { type: type, data: data, } return do_post(base_url + '/update-data', body) .then(json =>{ if( json.status != 'OK' ) throw "post failed"; }); }次はHTMLです。
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> <title>オリエンテーション ナビゲータ</title> <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> <script type="text/javascript" src="//maps.google.com/maps/api/js?key=【GoogleAPIキー】"></script> </head> <body> <div id="top" class="container"> <h1>オリエンテーション ナビゲータ</h1> <ul class="nav nav-tabs"> <li role="presentation" class="active"><a href="#oriatation" v-on:click="orientation_update_view" data-toggle="tab">オリエンテーション</a></li> <li role="presentation"><a href="#checkpoint" data-toggle="tab">チェックポイント</a></li> <li role="presentation"><a href="#myspot" data-toggle="tab">マイスポット</a></li> </ul> <div class="tab-content"> <div id="oriatation" class="tab-pane fade in active"> <br> <span class="form-inline"> <select class="form-control" v-model="travelmode"> <option value="walking">walking</option> <option value="bicycling">bicycling</option> <option value="driving">driving</option> <option value="transit">transit</option> </select> </span> <div class="btn-group"> <button class="btn btn-primary" v-on:click="orientation_next">{{orientation_text}}</button> <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button> <ul class="dropdown-menu"> <li><a v-on:click="orientation_restart(-1)">最初から再開</a></li> <li><a v-on:click="orientation_restart(origin_index)">今のチェックポイントを再開</a></li> </ul> </div> <span v-if="checkpoints.length >= 2"> <br><br> <label>現在地:</label>{{origin_index + 1}} {{checkpoints[origin_index].name}}, <label>目的地:</label>{{get_next_destination() + 1}} {{checkpoints[get_next_destination()].name}} </span> <button class="btn btn-default btn-xs pull-right" v-on:click="map_goto_current_location">現在地へ</button> <br> <div class="row" id="map_canvas" style="margin: 15px; height:600px"></div> </div> <div id="checkpoint" class="tab-pane fade in"> <br> <button class="btn btn-primary" v-on:click="do_checkpoints_reset">チェックポイントのリセット</button> <table class="table table-striped"> <thead> <tr><th>#</th><th>名前</th><th>経由地</th><th>編集</th></tr> </thead> <tbody> <tr v-for="(point, index) in checkpoints"> <td width="1px"> <div class="form-inline"> <select v-bind:value="index" v-on:change="do_checkpoint_change_index(index, $event)"> <option v-for="(point2, index2) in checkpoints" v-bind:value="index2" v-bind:selected="index==index2">{{index2 + 1}}</option> </select> </div> </td> <td> <button class="btn btn-default btn-xs" v-on:click="do_checkpoint_delete(index)">削除</button> {{point.name}} </td> <td> <input v-if="index!=0 && index!=(checkpoints.length-1)" type="checkbox" v-model="point.waypoint"> </td> <td> <div class="btn-group"> <button class="btn btn-default btn-sm" v-on:click="do_checkpoint_change_name(index)">名前</button> <button class="btn btn-default btn-sm" v-on:click="do_checkpoint_change_location(index)">場所</button> </div> </td> </tr> <tr> <td></td> <td> <div class="btn-group"> <button class="btn btn-default btn-sm" v-on:click="do_checkpoint_append">チェックポイント追加</button> <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button> <ul class="dropdown-menu"> <li><a v-on:click="do_checkpoint_append_myspot">マイスポットから追加</a></li> </ul> </div> </td><td></td><td></td> </tr> </tbody> </table> </div> <div id="myspot" class="tab-pane fade in"> <br> <table class="table table-striped"> <thead> <tr><th>#</th><th>名前</th><th>緯度</th><th>経度</th><th>編集</th></tr> </thead> <tbody> <tr v-for="(spot, index) in myspots"> <td width="1px">{{index + 1}}</td> <td><button class="btn btn-default btn-xs" v-on:click="do_myspot_delete(index)">削除</button> {{spot.name}}</td> <td>{{spot.lat.toFixed(7)}}</td><td>{{spot.lng.toFixed(7)}}</td> <td> <div class="btn-group"> <button class="btn btn-default btn-sm" v-on:click="do_myspot_change_name(index)">名前</button> <button class="btn btn-default btn-sm" v-on:click="do_myspot_change_location(index)">場所</button> </div> </td> </tr> <tr> <td></td> <td> <button class="btn btn-default btn-sm" v-on:click="do_myspot_append">地図から追加</button> </td> <td></td><td></td><td></td> </tr> </tbody> </table> </div> <br> <br> </div> <modal-dialog size="lg" id="orientation_complete_dialog"> <div slot="content"> <div class="modal-header"> <h4 class="modal-title">オリエンテーション達成</h4> </div> <div class="modal-body"> <center> オリエンテーション達成です。おめでとうございます。<br> <img src="img/goal_figure.png"> </center> </div> <div class="modal-footer"> <button class="btn btn-default" v-on:click="dialog_close('#orientation_complete_dialog')">閉じる</button> </div> </div> </modal-dialog> <modal-dialog size="lg" id="select_location_dialog"> <div slot="content"> <div class="modal-header"> <h4 class="modal-title">{{dialog_params.title}}</h4> </div> <div class="modal-body"> <div class="form-inline"> <button class="btn btn-default" v-on:click="dialog_submit" v-if="dialog_params.is_input_submit">この場所にする</button> <span v-if="dialog_params.is_input_name"> <label>名前</label> <input type="text" class="form-control" v-model="dialog_params.name"> </span> </div> <div class="row" id="map_canvas2" style="margin: 20px; height:300px"></div> </div> <div class="modal-footer"> <button class="btn btn-default" v-on:click="dialog_close('#select_location_dialog')">キャンセル</button> </div> </div> </modal-dialog> <!-- for progress-dialog --> <progress-dialog v-bind:title="progress_title"></progress-dialog> </div> <script src="js/start.js"></script> </body>もう長すぎてわけわかんないですよね。。。
その他ユーティリティのファイル等含めて、GitHubに上げています。セットアップ:GoogleMap API利用の準備
GoogleMapを利用するには、GoogleからAPIキーを払い出してもらう必要があります。
以下のサイトの通りに実施すれば、特に問題はなかったです。Get Started with Google Maps Platform
https://developers.google.com/maps/gmp-get-started?hl=ja最後に、API Keyを生成するのですが、Web APIsのMaps Embed APIを採用しました。
セットアップ:サーバの展開
以下のGitHubから一式ダウンロードしておきます。
poruruba/orientation_navigator
https://github.com/poruruba/orientation_navigatorそして、以下の通りに実行します。
unzip orientation_navigator.zip
cd orientation_navigator
npm install
mkdir data
node app.js以下、修正が必要です。
public/start.js
7行目のあたり
const base_url = "http://localhost:10080";
上記を立ち上げたサーバのURLを指定します。
public/index.html
34行目あたり
<script type="text/javascript" src="//maps.google.com/maps/api/js?key=【GoogleAPIキー】"></script>
また、HTML5の現在地情報取得機能を使っているのですが、それを利用するには、HTTPSである必要があります。
mkdir certs
このディレクトリに、SSL証明書類を配置しましょう。
以下が参考になります。
SSL証明書を取得しよう以上
- 投稿日:2020-05-24T22:59:27+09:00
async/await(Promise)のreturnとエラーハンドリング
目的
JavaScriptにおけるasync/awaitとPromiseは同じものですが、
エラーハンドリングに関しては違いがあったり、returnに気をつけないといけなかったり、
はまったり、見落としたりする箇所があるので、まとめておきます。async/awaitとPromiseの関係性については、記事中で詳しくは触れません。
おさらい
Promise
正常系
// 非同期でメッセージがかえってくる const pResult = Promise.resolve("結果です"); // 結果が帰ってきてないので、Promise型 console.log(pResult.toString()); // "[object Promise]" // thenで結果を待つ pResult.then(result => { console.log(result.toString()); // "結果です" });エラー発生
// 非同期でErrorがthrowされた状態 // RESTでAPIエラー起こすなどでここに入る const pError = Promise.reject(new Error("エラーです")); // 結果が帰ってきてないので、Promise型 console.log(pError.toString()); // "[object Promise]" // catchでエラーを取る pError .then(result => { console.log("到達しない!"); }) .catch(error => { console.log(error.toString()); // "Error: エラーです" });async/await
正常系
// async(Promise)内でしかawaitできない (async () => { // 非同期でメッセージがかえってくる const pResult = Promise.resolve("結果です"); // 結果が帰ってきてないので、Promise型 console.log(pResult.toString()); // "[object Promise]" // awaitで結果を待つ const result = await pResult; console.log(result.toString()); // "結果です" })()エラー発生
(async () => { // 非同期でErrorがthrowされた状態 // RESTでAPIエラー起こすなどでここに入る const pError = Promise.reject(new Error("エラーです")); // 結果が帰ってきてないので、Promise型 console.log(pError.toString()); // "[object Promise]" // throwされるので、tryでエラーを取る try { await pError; console.log("到達しない!"); } catch (error) { console.log(error.toString()); // "Error: エラーです" } })()returnとエラーハンドリング
どのようにreturnするかは、影響が大きいので、きちんと考えて書きます。
結果としてreturn書かなくても十分な場面は多いですが、returnを考慮した上で、書く、書かないを考えていきたい場所です。returnの考慮が必須になる場面が3パターンほどあります、注意していきましょう。
- 1. 非同期の結果を使う場合
- 2. エラーが発生する場合
- 3. 複数の非同期がある場合
1. 非同期の結果を使う場合
returnの有無でどうなるか、基本的な挙動なのでとりあえず覚えておきましょう。
returnを省略すると、undefinedになる
// Promise const pResult = Promise.resolve("結果です"); pResult .then(result => { console.log(result) // "結果です" // returnしない! }) .then(result => { // 結果が引き継がれない! console.log(result) // undefined })async/awaitの場合、returnしなかったらundefinedになるのは直感的ですね。
// async/await const pResult = Promise.resolve("結果です"); (async () => { async function edit1(){ const result = await pResult; console.log(result) // "結果です" // returnしない! } const edited1 = await edit1(); console.log(edited1) // undefined })()returnは、後続の引数になる
// Promise const pResult = Promise.resolve("結果です"); pResult .then(result => { console.log(result) // "結果です" return `${result} 2回目`; }) .then(result => { console.log(result) // "結果です 2回目" })// async/await const pResult = Promise.resolve("結果です"); (async () => { async function edit1(){ const result = await pResult; console.log(result) // "結果です" return `${result} 2回目`; } const edited1 = await edit1(); console.log(edited1) // "結果です 2回目" })()2. エラーが発生する場合
正常系とほぼ同じ挙動ですが、
1つの非同期に対して、2パターン(正常、エラー)は常に考えていくので、考慮するパターンが純粋に増えていきます。
考慮するパターンが多いと、バグる確率がどんどん上がってきます。catchでreturnを省略すると、undefinedで復旧する
catch後は正常系に戻りますが、returnを省略すると、後続の引数はundefinedになります。
なにかエラー処理をはさんだ時に、意図しない復旧をしてしまう場合があるので、注意です。// Promise const pError = Promise.reject(new Error("エラーです")); pError .catch(error => { console.log(error.toString()); // "Error: エラーです" // returnなし }) .then(result => { console.log(result); // undefined })こちらもasync/awaitであれば、直感的だと思います。
しかし、個人的にtry~catchは、Promiseでのcatchよりも視認性が悪いので好きではないです。// async/await const pError = Promise.reject(new Error("エラーです")); (async () => { async function edit1(){ try { return await pError; } catch(error){ console.log(error.toString()); // "Error: エラーです" // returnなし } } const edited1 = await edit1(); console.log(edited1); // undefined })()catchで書いたreturnは、復旧時の引数になる
エラー後、正常系に戻しつつ、デフォルトの挙動を指定したい場合などに書きます。
// Promise const pError = Promise.reject(new Error("エラーです")); pError .catch(error => { console.log(error.toString()); // "Error: エラーです" return "デフォルト値"; }) .then(result => { console.log(result); // "デフォルト値" });// async/await const pError = Promise.reject(new Error("エラーです")); (async () => { async function edit1(){ try { return await pError; } catch(error){ console.log(error.toString()); // "Error: エラーです" return "デフォルト値"; } } const edited1 = await edit1(); console.log(edited1); // "デフォルト値" })()復習ですが、awaitをreturnし忘れると、正常系がundefinedになるので、バグです。
try { // これでは後続がundefined // await pError; // return必要 return await pError; } ...catch時、再throwでエラー継続
エラーハンドリングをした上で、後続の正常系を止めたい場合などは、throwします。
// Promise const pError = Promise.reject(new Error("エラーです")); pError .catch(error => { console.log(error.toString()); // "Error: エラーです" throw error; }) .then(result => { console.log("到達しない!"); }) .catch(error => { console.log(`${error.toString()} 2回目`); // "Error: エラーです 2回目" })// async/await const pError = Promise.reject(new Error("エラーです")); (async () => { async function edit1(){ try { return await pError; } catch(error){ console.log(error.toString()); // "Error: エラーです" throw error; } } try { const edited1 = await edit1(); console.log("到達しない!"); } catch(error){ console.log(`${error.toString()} 2回目`); // "Error: エラーです 2回目" } })()3. 複数の非同期がある場合
1つの非同期で、2つ(正常、エラー)考えることがありましたが、
複数の非同期がある場合、全てで正常とエラー考慮が必要になってくるので、かなりつらい気持ちになると思います。
しかし残念ながら、複数の非同期をwaitしながら実行したり、エラーになったら後続を止めたかったりするのは、頻出です。2回非同期をして、結果を使う
結果をわたすときはreturnします、return漏れるとundefinedです。
実はthenはflatMapになっているので、Promiseをreturnしても、result2でPromiseはネストになりません。// Promise const pResult1 = Promise.resolve("結果 1回目"); pResult1 .then(result1 => { // 1回目の結果を使って、2回目の非同期をする return Promise.resolve(`${result1} 2回目`); }) .then(result2 => { // result2はPromiseではない console.log(result2); // "結果 1回目 2回目" })// async/await const pResult1 = Promise.resolve("結果 1回目"); (async () => { async function edit1(){ const result1 = await pResult1; return Promise.resolve(`${result1} 2回目`); } const result2 = await edit1(); console.log(result2); // "結果 1回目 2回目" })()1つ目の非同期でエラーが起きたら、2つ目の非同期は実行しないが、正常系に戻す
returnさえ忘れなければ、おかしな動作はないです。
もし2つ目のPromiseをreturnし忘れ、エラーが発生した場合、catchに入らなくなるので、結構ハマります。// Promise const pResult1 = Promise.reject(new Error("エラーです")); pResult1 .then(result1 => { return Promise.resolve(`${result1} 2回目`); }) .catch(error => { return "デフォルト値"; }) .then(result2 => { console.log(result2); // "デフォルト値" })// async/await const pResult1 = Promise.reject(new Error("エラーです")); (async () => { async function edit1(){ const result1 = await pResult1; return Promise.resolve(`${result1} 2回目`); } let result2; try { result2 = await edit1(); } catch(error){ result2 = "デフォルト値"; } console.log(result2); // "デフォルト値" })()余談ですが、async/awaitだからといって、try~catch文を使わないといけないわけでは無いです。
Promiseのcatchを混ぜた方が綺麗です。// async/await + Promise#catch const pResult1 = Promise.reject(new Error("エラーです")); (async () => { async function edit1(){ const result1 = await pResult1; return Promise.resolve(`${result1} 2回目`); } const result2 = await edit1().catch(error => "デフォルト値"); console.log(result2); // "デフォルト値" })()
- 投稿日:2020-05-24T22:32:27+09:00
Day23 Javascriptメソッドまとめ
window.alert('')
window.alert('表示したい文');console.log('')
console.log('こんばんは');変数の宣言
let
,const
let name = 'yamada'; console.log(name + 'さん、こんばんは');条件分岐
if
,else if
let num = 60; if (num % 15 == 0) { console.log(num + 'は3と5の倍数です'); } else if (num % 3 == 0) { console.log(num + 'は3の倍数です'); } else if (num % 5 == 0) { console.log(num + 'は5の倍数です'); } else { console.log(num + 'は3の倍数でも、5の倍数でもありません'); } =>60は3と5の倍数です*条件式は
()
内に記述。実行内容は{}
で記述。javascript配列
1.配列の要素取得
let list = []
let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS']; console.log(list[2]); => javascript2.配列の要素数取得
length
let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS']; console.log(list.length); => 53.配列の要素追加
push
let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS']; list.push('GitHub'); console.log(list);4.配列の要素削除
pop
shift
let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS']; list.pop(); console.log(list); =>最後の要素の'CSS'が削除let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS']; list.shift(); console.log(list); =>最初の要素の'Ruby'が削除*Rubyでは取り除く要素数を指定できますが、JavaScriptではできません。
オブジェクト
{プロパティ名: '値'}
let obj = { name: 'yamada' };
name
はプロパティ、yamada
は値繰り返し
for
for (let i = 0; i < 繰り返す回数; i = i + 1) { // 繰り返す処理の内容 }関数の定義
function 関数名(引数)
function sayHello(){ console.log('hello'); } function sayName(name){ console.log(name); } let myName = 'yamada'; sayHello(); sayName(myName); =>hello yamada*引数がない場合も()は記述必要
返り値
return
function calc(num1,num2){ num1*num2; } let num1 = 3; let num2 = 4; console.log(calc(num1,num2)); =>出力されない function calc(num1,num2){ return num1*num2; } let num1 = 3; let num2 = 4; console.log(calc(num1,num2)); => returnで返り値を明治。
- 投稿日:2020-05-24T21:59:10+09:00
Javascriptで作る モーダルウィンドウ
まず、モーダルを作る場合、部品が何個あるのか考えます。
今回のモーダルウィンドウでいうと、
①モーダルを呼び出すボタン
②モーダルが出てきた際の背景が黒い画像
③モーダルウィンドウ大きく分けて、この3つが必要となる。
①モーダルを呼び出すボタン
<section class="container"> <p>ボタンを押すとモーダルウィンドウが開きます</p> <button type="button" class="btn">開く</button> </section>②モーダルが出てきた際の背景が黒い画像
<div class="mask" id="blackMask"></div>③モーダルウィンドウ
<section class="modal" id=""> <div class="top"> <h1>Vue.js Modal Window!</h1> <input type="text"> </div> <div class="under"> <button type="button" class="btn">送信</button> </div> </section>次にCSSで非表示設定、Javascriptで動きを付けていく
①mask と modalを非表示にする(CSS)
1.display: none;
2.transform: translateY(150px,-500px);
※モーダルは上から落ちてくる感じにしたいので、translateY(-500px);で上に置いとく
※SCSSの場合は、CSSではなく、IDの方にクラスを付けるため、{}の外側に書くconst open = document.getElementById('open'); const idMask = document.getElementById('idmask'); const idModal = document.getElementById('idmodal'); open.addEventListener('click', () => { idMask.classList.add('active'); idModal.classList.add('drop'); }); idMask.addEventListener('click', () => { idMask.classList.remove('active'); idModal.classList.remove('drop'); });基本的には、要素を取得してきてclickイベントで要素の付け外しを行う
おまけ
.modal { background: #ffffff; width: 200px; border-radius: 4px; transform: translate(150px,-500px); transition: transform 0.4s;transition: transform 0.4s;を設定してあげることで、上からゆっくりと
モーダルが落ちてくる。
- 投稿日:2020-05-24T21:38:19+09:00
@kintone/rest-api-client への移行を考える
えっ!? kintone JS SDK が・・・!!?
kintone の主戦場である JavaScript/TypeScript 界隈で kintone とのやりとりをカプセル化・抽象化するツールとして欠かせない存在である kintone JS SDK。
なんといつの間にか deprecated になっていました。
最終バージョンは 0.7.8 と、結局正式版リリースまで到達せず終了と言う事になりそうです。
旧 JS SDK の代替として 2020 年 2 月に @kintone/rest-api-client 正式版がリリースされていました。
Git リポジトリのパスが妙に深いのが気になるところではありますが、執筆時点(2020 年 5 月下旬)では最新版が 1.3.0 まで上がって来ており、一般的な利用の範囲内では旧 JS SDK を置き換えられるものと判断して良いのではないかと思われます。kintone 開発キットの数奇な運命
思い起こせば kintone の開発キットは波乱含みであったように思います。
時系列で並べてみると、
時期 出来事 2018 年 5 月 kintone API SDK(β) for Node.js(kintone-nodejs-sdk)リリース 2019 年 3 月 kintone JS SDK リリース 2020 年 2 月 kintone JavaScript Client (@kintone/rest-api-client) リリース なんと! 初代、2 代目と、共に 1 年未満で次期ツールがリリースされると共に deprecated の憂き目を見ています!
短命で知られるジョースター家の男子だってもう少し長生きするよ!と言う感じです。@kintone/rest-api-client の特徴
新しい SDK、
@kintone/rest-api-client
はそれ自体が TypeScript で開発されており、旧 JS SDK が抱えていた(そして kintone extension でも解決できなかった)コード補完効かない問題を解決しています。
旧 JS SDK にd.ts
ファイルが結局最後まで用意されなかった事を考えれば隔世の感がありますね。旧 JS SDK では、確か 0.3 から 0.4 へのバージョンアップ時(0.4 から 0.5 の時だったかも)、関数に引数を渡す方式がそれまでの変数の羅列からオブジェクトを引き渡す形式に変わり数字以上の大きな影響がありましたが、@kintone/rest-api-client は初めからオブジェクトを引き渡す形になっています。
まあ昨今の潮流から言っても自然ですね。他にも、同じ機能を受け持つ関数の名前が変わったり、戻り値の形が変わっていたりと、基本的には「そりゃこうあるべきだろ」と言うポジティブな方向にブラッシュアップされており、旧 JS SDK が抱えていた不自然さがだいぶ改善されています。
と言う事で、この記事では 3 代目ジョジョ、もとい 3 代目 SDK である @kintone/rest-api-client について、旧 JS SDK からの移行と言う観点で各種レコード操作の実際のコードの違いを含めて見ていきたいと思います。
@kintone/rest-api-client を試す
以下の解説では、他の記事 でも解説している kintone 側で用意している雛形アプリの 1 つ「案件管理」アプリを利用しています。
以下のような下処理をやっています。
- アプリを作成時、「サンプルデータを含める」で作成
- 各フィールドのフィールドコードをフィールド名と合わせておく
- プロジェクトは kintone-cli で作成
- 作成したプロジェクトを開き
@kintone/rest-api-client
をインストール
kintone-cli
についてもいつか記事を書きたいと思っています。さて、プロジェクトの準備ができたら早速動作を見てみます。
良く使う関数類を確かめてみましょう。なお、確認のために使用したコードは GitHub にアップしています。
レコード 1 件取得・レコード複数取得
getRecord()
及びgetRecords()
に関しては 旧 JS SDK との間でリクエスト・レスポンスの違いはありませんでした。
クエリの指定の仕方も変わりません。@kintone/rest-api-clientのgetRecords()const result = await new KintoneRestAPIClient().record.getRecords({ app: appId, fields: ["レコード番号", "確度", "会社名"], query: '確度 in ("A") order by レコード番号 asc', });{ "records": [ // 省略 ], "totalCount": null }レコード一括取得
レコード一括取得は関数名が違います。
旧 JS SDK ではgetAllRecordsByQuery()
,getAllRecordsByCursor()
と言う関数名でしたが、@kintone/rest-api-client では目的に応じてgetAllRecords()
,getAllRecordsWithId()
,getAllRecordsWithOffset()
,getAllRecordsWithCursor()
と 4 つに分かれています。
これらの関数は旧 JS SDK とは渡す引数の構成も違って来ており、単純に関数名を一括置換するだけでは移行できないので注意が必要です。旧JSSDKのgetAllRecordsByQuery()const result = await new kintoneJSSDK.Record().getAllRecordsByQuery({ app: appId, fields: ["レコード番号", "確度", "会社名"], query: '確度 in ("A") order by レコード番号 asc', });@kintone/rest-api-clientのgetAllRecords()const result = await new KintoneRestAPIClient().record.getAllRecords({ app: appId, fields: ["レコード番号", "確度", "会社名"], condition: '確度 in ("A")', orderBy: "レコード番号 asc", });
query
の部分がcondition
に変わっており、order by
はorderBy
パラメータに切り出す必要があるのが見て取れますね。ただ、
getAllRecordsWithCursor()
関数は、@kintone/rest-api-clientのgetAllRecordsWithCursor()const result = await new KintoneRestAPIClient().record.getAllRecordsWithCursor({ app: appId, fields: ["レコード番号", "確度", "会社名"], query: '確度 in ("A") order by レコード番号 asc', });と、旧 JS SDK の
getAllRecordsByCursor()
と同じような書き方が出来ます。レスポンスに関しては、旧 JS SDK が
旧JSSDKのレスポンス{ "records": [ // 省略 ], "totalCount": 10 }と言う形で返って来るのに対し、@kintone/rest-api-client では
@kintone/rest-api-clientのレスポンス[ // 省略 ]と、ちょうど
records[]
の中身に相当するものだけが返却されます。従って、旧 JS SDK で
getAllRecordsBy****()
を使用していた部分は、それなりに慎重に書き換えが必要になります。レコード 1 件登録
これは両 SDK とも変わりはありません。
レスポンスの形式も同じです。レコード複数登録
これも両 SDK とも変わりはありません。
100 件までの同時登録が可能と言う仕様も変わりません。レスポンスは、@kintone/rest-api-client では以下のように
@kintone/rest-api-clientのレスポンス{ ids: [ (省略) ], records: [ { id: 'xx', revision: 'xxx' }, ... ], revisions: [ (省略) ], }と、旧 JS SDK にはなかった
records[]
が含まれるようになっています。
情報としては重複していますが、使い勝手は良くなっていると言えるでしょう。レコード一括登録
リクエストの形式は変わらず、100 件以上のレコード登録時に利用すると言う目的も同じです。
一方、レスポンスは大きく異なります。
旧 JS SDK では
旧JSSDKのレスポンス{ "results": [ { "ids": [ (省略) ], "revisions": [ (省略) ] }, ...(省略) ] }となっていたのに対し、@kintone/rest-api-client では
@kintone/rest-api-clientのレスポンス{ "records": [{ "id": "xx", "revision": "xxx" }, ...(省略)] }となっています。
addRecords()
のrecords[]
のみが取り出された格好です。
addAllRecords()
は内部的にはbulkRequest API
をコールしており、もともとこの API の目的が複数アプリのレコード一括操作であることから、旧 JS SDK ではresults[]
と言う配列に格納されて結果が返される仕様だったわけです。
しかしながら正直なところ SDK の関数を利用する側からすれば無駄に複雑で意味不明に見えるレスポンス形式であったのは否めません。
(addAllRecords()
関数は複数アプリに対してどうこうと言う操作ができるわけではないので)
@kintone/rest-api-client では、この不合理を解決し、あるべき形でレスポンスを戻すようになりました。
非常に使いやすくなったと言える一方、旧 JS SDK からの移行に関しては注意を要する箇所です。レコード 1 件更新
これは両 SDK とも変わりはありません。
レスポンスの形式も同じです。レコード複数更新
これも同じ。関数名の書き換えだけで OK です。
なお、@kintone/rest-api-client の
updateRecord()
等いくつかの関数では、コード補完で表示されるポップアップの内容に誤りが見られます。
これってどこ由来?と思って GitHub 上の当該ソース やnode_modules
のd.ts
ファイル等を確かめてみたのですが、それらでは本来あるべき値になっている。
どういう理由でここがおかしくなっているのか不明です。
VS Code 側のバグ?筆者の環境だけかしら?レコード一括更新
上述の レコード一括登録と同様、レスポンスの形が大きく異なります。
旧 JS SDK では
旧JSSDKのレスポンス{ "results": [ { "records": [{ "id": "xx", "revision": "xxx" }, ...(省略)] }, ...(省略) ] }ですが、@kintone/rest-api-client では
@kintone/rest-api-clientのレスポンス{ "records": [{ "id": "xx", "revision": "xxx" }, ...(省略)] }となります。
旧 JS SDK のresults[0]
が取り出されて返って来ている格好です。レコード複数削除
これは両 SDK とも変わりはありません。
レスポンスの形式も同じで、全レコード正常削除できれば{}
が返ります。なお、@kintone/rest-api-client の
deleteRecords()
や次に触れるdeleteAllRecords()
はupdateRecords()
等と同様コード補完に誤りがあるので注意です。レコード一括削除
レコード一括削除は両 SDK 間でコンセプトが大きく異なっています。
旧 JS SDK では旧JSSDKのdeleteAllRecordsByQuery()const result = await new kintoneJSSDK.Record().deleteAllRecordsByQuery({ app: appId, query: '確度 in ("A")', });と言った具合に、クエリで削除対象とする条件を指定する事ができました。
一方 @kintone/rest-api-client では@kintone/rest-api-clientのdeleteAllRecords()const result = await new KintoneRestAPIClient().record.deleteAllRecords({ app: appId, records: [{ id: "1" }, { id: "2" }, { id: "3" }], });と言った形であくまで ID (とリビジョン)で構成されるオブジェクトの配列で対象を指定する形になります。
@kintone/rest-api-client にはクエリで削除対象を抽出して削除、と言う機能を持つ関数は実装されていません。これは恐らく、削除対象をクエリで指定すると言うのは kintone 側に対象抽出を丸投げしてしまう事になり、しかもレスポンスにはどのレコードを削除したと言う明確な結果が返って来ない点を不合理だと判断したものと思われます。
そりゃ「削除したいなら削除したいものを明示してくれよ」と要求するのは当然ですよね。
で、その @kintone/rest-api-client で指定するrecords
ですが、これはaddRecords()
等で返却されるrecords
オブジェクトと同じ形式で、その意味でも @kintone/rest-api-client の考え方には一貫性があります。戻り値も形式が異なります。
旧 JS SDK では
旧JSSDKのレスポンス{ "results": [{}] }ですが、@kintone/rest-api-client では
@kintone/rest-api-clientのレスポンス{}です。
まとめ
各関数について、単純に置き換え可能か、修正が必要かどうかをまとめると以下のようになります。
処理内容 kintone JS SDK @kintone/rest-api-client 移行時のコード修正 レコード 1 件取得 getRecord() getRecord() 不要 レコード複数取得 getRecords() getRecords() 不要 レコード一括取得(クエリ) getAllRecordsByQuery() getAllRecords() かなり修正が必要(渡す引数と戻り値の形式が異なる) レコード一括取得(カーソル) getAllRecordsByCursor() getAllRecordsWithCursor() 修正が必要(戻り値の形式が異なる) レコード 1 件登録 addRecord() addRecord() 不要 レコード複数登録 addRecords() addRecords() 不要 レコード一括登録 addAllRecords() addAllRecords() 修正が必要(戻り値の形式が異なる) レコード 1 件更新 updateRecordById() updateRecord() 不要 レコード複数更新 updateRecords() updateRecords() 不要 レコード一括更新 addAllRecords() addAllRecords() 修正が必要(戻り値の形式が異なる) レコード複数削除 deleteRecords() deleteRecords() 不要 レコード一括削除 deleteAllRecordsByQuery() deleteAllRecords() かなり修正が必要(関数のコンセプト自体が異なる) 上述の「不要」の部分にしても、
旧JSSDKconst result = await new kintoneJSSDK.Record().getRecords({ app: appId, });を
@kintone/rest-api-clientconst result = await new KintoneRestAPIClient().record.getRecords({ app: appId, });と書き換える程度の修正は当然必要ですのでご注意ください。
感想
と言う事で、新たにリリースされたクライアントツールであるところの @kintone/rest-api-client についてざっと見て来ました。
もちろん他にもカーソルだったりコメントであったりの操作もあるので、これで全部と言うわけではありません。異常系のパターンも見てませんしね。最初の方でも述べましたが、 @kintone/rest-api-client は現時点で既に旧 JS SDK の大部分の機能をカバーしており、新規プロジェクトにおいては旧 JS SDK ではなくこちらをファーストチョイスとして問題ないクォリティでしょう。
一方、既に旧 JS SDK を導入済みのプロジェクトでは、関数の名前や引数、レスポンスの形式がそれなりに異なることから、安易な移行(置き換え)には慎重さが求められそうです。初めの方に見た通り、kintone の SDK は 2 代連続で 1 年程度で deprecated の憂き目を見ており、当代であるところの 3 代目が来年の夏を無事に迎えられるかは今は判りません・・・が、さすがにそろそろこれが決定版!と言えるようなものになって欲しいなと思います。
kintone は開発者の環境を良くするために様々なツールをリリースする事に非常に積極的で、そのおかげで 開発者ドキュメント を上から下まで全部読み込まなければカスタマイズなんて碌にできません、みたいな時代は既に過ぎ去ったと言えるでしょう。
ただその分、それぞれ個々のツールは若干機能不足が散見されたり、メンテナンスが止まったり、今回のように短期間で代替ライブラリへの移行が発生したりと、どことなくこれらのプロジェクトに関わる全体としてリソースが不足気味なのかなあと感じさせる場面も。
そんなん言うならお前がコントリビューターとして貢献しろよ。って話かも知れませんがね。と言うわけで @kintone/rest-api-client のざっくりレビューでした!
どちら様も良き kintone ライフを!参考
- 投稿日:2020-05-24T21:36:31+09:00
Java Scriptの基礎
JavaScriptとは
JavaScript
とは、プログラミング言語の一つで、1990年代中盤に登場しました。
サイトのプルダウンや画面を更新しないでサーバーと通信したい時に使われます。
略称は、JS
です。HTMLへの導入方法
htmlファイルと同じ階層にJSファイルがある場合htmlファイルに以下のように記述するとJSファイルが読み込まれます。
index.html<head> <script src="script.js"></script> </head>headタグ内にある、
scriptタグ
のsrc
属性にファイル名を記述します。JSの拡張子は.js
です。window.alert()
ブラウザにアラートを表示させるメソッドです。
表示させたい値を引数にとります。また、変数でも可能です。console.log()
ブラウザのコンソールにテキストを表示させるメソッドです。
表示させたい値を引数にとります。また、変数でも可能です。変数での記述方法は以下になります。
script.jsvar team = "鹿島アントラーズ"; console.log( team + "の勝利です。");コンソールが面に 鹿島アントラーズの勝利です。 が表示されます。
変数宣言について
var
はES6バージョンの書き方です。
それ以降の変数宣言の仕方としては。
・let
・const
の二つが用いられます。
let
は、後で書き換えられ変数宣言です。
const
は、後で書き換えられない変数宣言です。条件分岐
script.jslet number = 100; if (number % 15 == 0) { console.log(number + "は、3と5の倍数です。"); } else if (number % 3 == 0) { console.log(number + "は、3の倍数です。"); } else if (number % 5 == 0) { console.log(number + "は、5の倍数です。"); } else { console.log(number + "は、3の倍数でも、5の倍数でもありません"); }上記のように記述するとコンソールに、100は、5の倍数です。 と表示されます。
ifの後のカッコ内に条件式を記述します。その条件に当てはまった場合は、波カッコ無いの記述が処理されます。
最初の条件式が偽だった場合は、else if以降に処理が移ります。その条件が真だったら波カッコ内の処理を開始、偽だった場合はさらに次に進みます。elseはどの条件にも当てはまらなかった場合に出力したい処理を記述します。配列
JSには配列という概念があります。
script.jslet name = ["takuya", "shingo", "tsuyoshi", "masahiro", "GORO"]; console.log(name); ##コンソールでの表示 (5) ["takuya", "shingo", "tsuyoshi", "masahiro", "GORO"] ---------配列の数を取得--------- lengthメソッドを使います。 ------------------------------ console.log(name.length); ##コンソールでの表示 5 ---------配列の最後に要素を追加--------- pushメソッドを使います。 ------------------------------------- name.push("morikun"); console.log(name); ///"morikun"が追加されます。 ##コンソールでの表示 (6) ["takuya", "shingo", "tsuyoshi", "masahiro", "GORO", "morikun"] ---------配列の最後の要素を削除--------- popメソッドを使います。 ------------------------------------- name.pop(); console.log(name); ///"morikun"が削除されます。 ##コンソールでの表示 (5) ["takuya", "shingo", "tsuyoshi", "masahiro", "GORO"] ---------配列の最初の要素を削除--------- shiftメソッドを使います。 ------------------------------------- name.shift(); console.log(name); ///"takuya"が削除されます。 ##コンソールでの表示 (4) ["shingo", "tsuyoshi", "masahiro", "GORO"]popメソッドとshiftメソッドに指定して複数の要素を削除することはできません。
オブジェクト
波カッコを使ってオブジェクトを生成します。
{}内に名前と値をセットにして管理します。このセットのことをプロパティと言います。
最初からオブジェクトにプロパティを定義して生成することもできますし、空の波カッコで生成することもできます。script.jslet takuya = { name: "kimura", age: 40, team: "SMAP" }; console.log(takuya); ###コンソールでの表示 {name: "kimura", age: 40, team: "SMAP"} ///名前がteamにあたる値を取得 console.log(takuya.team); ###コンソールでの表示 SMAP ///名前がageにあたる値を更新 takuya.age = 47; console.log(takuya.age); ###コンソールでの表示 47for文
繰り返しの構文です。
for(let i = 0; i < 繰り返す回数; i += 1) {繰り返す処理}
と記述します。script.jsnum = 1; for (let i = 0; i < 10; i += 1) { console.log(num + "回目の出力になります!"); num += 1; } ###コンソールでの表示 1回目の出力です。 2回目の出力です。 3回目の出力です。 4回目の出力です。 5回目の出力です。 6回目の出力です。 7回目の出力です。 8回目の出力です。 9回目の出力です。 10回目の出力です。
- 投稿日:2020-05-24T21:22:24+09:00
mapboxでお手軽に人工衛星の軌道を投影する
人工衛星がどの位置にあるのか、mapbox上にパッと表示する方法です。
地図は地理院タイルを利用します。
衛星の位置はTwo Line Elements(TLE)をcelestrakから入手します。
TLEを緯度経度に変換するアルゴリズムは、sgp4が有名です。
このアルゴリズムは観測から15日後の衛星位置を数10km程度の誤差で予測できるすぐれものです。自前で作ろうと思いましたが、アルゴリズムの中身を見ると精度が良いだけに面倒だったので、素直に既存のライブラリに頼ることにします。
node-sgp4またユーザがTLEを入力できるようにしたいので、入力値がTLEをとして適切かバリデーションするのにもライブラリをお借りすることにします。
tle-validatorサンプル
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>TLE</title> <script src='https://api.mapbox.com/mapbox-gl-js/v1.9.1/mapbox-gl.js'></script> <link href='https://api.mapbox.com/mapbox-gl-js/v1.9.1/mapbox-gl.css' rel='stylesheet' /> <style> #map { height: 480px; } textarea { width: 450px; height: 3em; line-height: 1.5; font-size: 12px; } #error { font-size: 10px; } </style> </head> <body> <div id="map"></div> <div style="margin-top: 20px;"> <textarea id="tle"></textarea> </div> <div> <button id="add">追加</button> <p id="error"></p> </div> </body> <script src='./dist/tle.js'></script> </html>import SGP4 from 'sgp4'; import TLEValidator from 'tle-validator'; const simulateUpdateSecond = 60; const animationDurationMilliSecond = 50; const maxSecond = 3600 * 24; const map = new mapboxgl.Map({ container: "map", center: [139.765, 35.65], zoom: 1, minZoom: 1, maxZoom: 8 }); map.addLayer({ "id": "base/pale", "type": "raster", "source": { type: "raster", tiles: [ "https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png" ], attribution: "<a href='http: //maps.gsi.go.jp/development/ichiran.html'>地理院タイル</a>", tileSize: 256, minzoom: 2, maxzoom: 8 } }); map.on('load', function () { document.getElementById("add").addEventListener('click', function () { document.getElementById("error").innerText = ""; let texts = document.getElementById("tle").value.split(/\n/); function setAnimation(line1, line2) { const satRec = SGP4.twoline2rv(line1, line2, SGP4.wgs84()); const now = new Date(); const id = now.getTime(); const datetime = now; const counterclockwise = getAngularVelocity(datetime).z > 0; const meanMotion = satRec.no; //rad/min const orbitPeriod = 2 * Math.PI / (meanMotion / 60) //sec const limitSecond = Math.min(maxSecond, orbitPeriod) function getPosition(datetime) { //https://github.com/joshuaferrara/node-sgp4/blob/master/example.js const positionAndVelocity = SGP4.propogate(satRec, datetime.getUTCFullYear(), datetime.getUTCMonth() + 1, datetime.getUTCDate(), datetime.getUTCHours(), datetime.getUTCMinutes(), datetime.getUTCSeconds()); const gmst = SGP4.gstimeFromDate(datetime.getUTCFullYear(), datetime.getUTCMonth() + 1, datetime.getUTCDate(), datetime.getUTCHours(), datetime.getUTCMinutes(), datetime.getUTCSeconds()); const geodeticCoordinates = SGP4.eciToGeodetic(positionAndVelocity.position, gmst); const longitude = SGP4.degreesLong(geodeticCoordinates.longitude); const latitude = SGP4.degreesLat(geodeticCoordinates.latitude); return [longitude, latitude] } function getAngularVelocity(datetime) { const positionAndVelocity = SGP4.propogate(satRec, datetime.getUTCFullYear(), datetime.getUTCMonth() + 1, datetime.getUTCDate(), datetime.getUTCHours(), datetime.getUTCMinutes(), datetime.getUTCSeconds()); const position = positionAndVelocity.position; const velocity = positionAndVelocity.velocity; const posLength = Math.sqrt(position.x ** 2 + position.y ** 2 + position.z ** 2); const velLength = Math.sqrt(velocity.x ** 2 + velocity.y ** 2 + velocity.z ** 2); return { x: (position.y * velocity.z - position.z * velocity.y) / posLength / velLength, y: (position.z * velocity.x - position.x * velocity.z) / posLength / velLength, z: (position.x * velocity.y - position.y * velocity.x) / posLength / velLength } } map.addLayer({ 'id': 'line_' + id, 'type': 'line', 'source': { 'type': 'geojson', 'data': { 'type': 'FeatureCollection', 'features': [{ 'type': 'Feature', 'geometry': { 'type': 'LineString', 'coordinates': [ [0, 0] ] } }] } }, 'layout': { 'line-cap': 'round', 'line-join': 'round' }, 'paint': { 'line-color': '#ed6498', 'line-width': 2, 'line-opacity': 0.8 } }); map.addLayer({ 'id': 'point_' + id, 'source': { 'type': 'geojson', 'data': { 'type': 'Point', 'coordinates': [0, 0] } }, 'type': 'circle', 'paint': { 'circle-radius': 10, 'circle-color': '#007cbf' } }); function animate(timestamp) { return function () { let datetime = new Date(timestamp) let crossingAntiMeridianNumber = 0; let elapsedSec = 0; let lines = [ [] ]; let before = getPosition(datetime) while (elapsedSec < limitSecond) { let lonlat = getPosition(datetime); if (counterclockwise) { if (lonlat[0] < 0 && before[0] > 0) { const lat = (lonlat[1] - before[1]) / (360 + lonlat[0] - before[0]) * (180 - before[0]) + before[1]; lines[crossingAntiMeridianNumber].push([180, lat]) lines[++crossingAntiMeridianNumber] = [ [-180, lat] ] } } else { if (lonlat[0] > 0 && before[0] < 0) { const lat = (lonlat[1] - before[1]) / (-360 + lonlat[0] - before[0]) * (-180 - before[0]) + before[1]; lines[crossingAntiMeridianNumber].push([-180, lat]) lines[++crossingAntiMeridianNumber] = [ [180, lat] ] } } lines[crossingAntiMeridianNumber].push(lonlat); before = lonlat if (elapsedSec < limitSecond / 2) setTimeout(function () { map.getSource('point_' + id).setData({ 'type': 'Point', 'coordinates': lonlat }); }, 50 * elapsedSec); datetime.setSeconds(datetime.getSeconds() + simulateUpdateSecond) elapsedSec += simulateUpdateSecond; } function createFeature(line) { return { 'type': 'Feature', 'geometry': { 'type': 'LineString', 'coordinates': line } } } let geojson = { 'type': 'FeatureCollection', 'features': lines.map(function (line) { return createFeature(line) }) }; map.getSource('line_' + id).setData(geojson); setTimeout(animate(datetime.getTime()), animationDurationMilliSecond * elapsedSec / 2); } } animate(now.getTime())(); } if (texts.length > 1 && TLEValidator.validateTLE(texts[0].trim(), texts[1].trim())) { setAnimation(texts[0].trim(), texts[1].trim()); document.getElementById("tle").value = ""; } else { document.getElementById("error").innerText = "フォーマットが正しくありません。" } }) });Javascriptはwebpackでコンパイルしています。コンパイルせず使いたい際はライブラリをダウンロードして適宜読み込ませてください。
ロジック
mapboxのAnimate a lineとAnimate a pointをベースに人工衛星特有のロジックを加えています。
function getAngularVelocity(datetime) { const positionAndVelocity = SGP4.propogate(satRec, datetime.getUTCFullYear(), datetime.getUTCMonth() + 1, datetime.getUTCDate(), datetime.getUTCHours(), datetime.getUTCMinutes(), datetime.getUTCSeconds()); const position = positionAndVelocity.position; const velocity = positionAndVelocity.velocity; const posLength = Math.sqrt(position.x ** 2 + position.y ** 2 + position.z ** 2); const velLength = Math.sqrt(velocity.x ** 2 + velocity.y ** 2 + velocity.z ** 2); return { x: (position.y * velocity.z - position.z * velocity.y) / posLength / velLength, y: (position.z * velocity.x - position.x * velocity.z) / posLength / velLength, z: (position.x * velocity.y - position.y * velocity.x) / posLength / velLength } }
SGP4.propogate
は人工衛星の位置ベクトルと速度ベクトルを返します。
これを使って角速度ベクトルを求め回転方向を判別しています。const counterclockwise = getAngularVelocity(datetime).z > 0;経度180度線をまたぐ際はポリゴンを分割しないと表示がおかしなことになってしまうので配列を追加し、線が途切れないように線形補間で180度の点を与えています。
let lonlat = getPosition(datetime); if (counterclockwise) { if (lonlat[0] < 0 && before[0] > 0) { const lat = (lonlat[1] - before[1]) / (360 + lonlat[0] - before[0]) * (180 - before[0]) + before[1]; lines[crossingAntiMeridianNumber].push([180, lat]) lines[++crossingAntiMeridianNumber] = [ [-180, lat] ] } } else { if (lonlat[0] > 0 && before[0] < 0) { const lat = (lonlat[1] - before[1]) / (-360 + lonlat[0] - before[0]) * (-180 - before[0]) + before[1]; lines[crossingAntiMeridianNumber].push([-180, lat]) lines[++crossingAntiMeridianNumber] = [ [180, lat] ] } }表示は
animationDurationMilliSecond
[ms]ごとにsimulateUpdateSecond
[s]分進めます。
また軌道の予測線は、軌道maxRev
周期またはmaxSecond
[s]のより短い分描画します。
予測線は定期的に更新されます(元となるTLEを更新しているわけではないので、当然徐々に誤差が大きくなっていきます)。const simulateUpdateSecond = 60; const animationDurationMilliSecond = 50; const maxSecond = 3600 * 24; const maxRev = 1;衛星例
代表的な衛星はこちらにまとめられているので参考にしてください。
国際宇宙ステーションや今流行りのstarlink、
連邦破産法11条の適用申請したonewebを見ることができます。
モルニヤ軌道をとるmolniya衛星なんかも面白いです。この中に無い衛星のTLEはここで検索します。
例えば「みちびき」を調べたいときは「qzs」で検索すると見つかります。
参考
Two-line element set
きどうようそのひみつ
- 投稿日:2020-05-24T21:13:18+09:00
WebAssemblyをwindows上で使用する
はじめに
Web画面の開発などでは良くJavaScriptが使用されていますが複雑な処理を行うと動作が遅くなる場合があります。
そのため、より早くより効率的に開発をするためにCやC++といったコンパイル可能な言語を使用してWebの開発を可能にしたものがWebアセンブリになります。
Emscriptenを使用してWindowsの開発環境を作る方法は公式に記載されていますが、自分の悩んだところやエラーなどを含めてまとめてみました。環境
- windows 10
- python:3.6.5
- emcc:1.39.16
- clang:11.0.0
環境作成
2020/05/24時点で、EmscriptenはGit上からソースを取得してインストールを行う方法しかないようです。
GitのクライアントをWindowsに入れたくないため、GitのWebからソースをダウンロードしてインストールしています。ソースのダウンロード
Gitのサイトからダウンロードして解凍するか
クローンすることでソースを取得します。
解凍先のパスに日本語が入っていると後々エラーするため日本語のないパスの方が良いです。インストール
・コマンドプロンプトを起動する
※フォルダの場所によっては管理者として実行が必要・ソースを配置したフォルダに移動する
自分はC:\webassenbleに配置したので以下のコマンドになります。
コマンド:cd C:\webassenble\emsdk-master
・インストール
配置したソースの中にemsdkがあるはずなのでそれを使用してインストールを行います。
Emscriptenはバージョンの指定をしてインストール更新が可能になっており、今回はlatest(最新)を入れています。
このコマンドを実行した後に大体10分ほど時間がかかります。
コマンド:emsdk install latest
コマンド結果
Installing SDK 'sdk-releases-upstream-ae5001f ~~~~ 略 ~~~~~ Done installing SDK 'sdk-releases-・設定更新
Emscripten用の設定を行います。この設定はWindows自体には設定されずにユーザのフォルダの直下のファイルに設定されます。
コマンド:emsdk activate latest
コマンド結果
Writing .emscripten configuration file to user home directory ~~~~ 略 ~~~~~ dows Registry, rerun this command with the option --global.・設定反映
現在開いているコマンドプロンプトに環境設定を読み込みます。
コマンド:emsdk_env.bat
・インストール確認
インストールできたかを確認します。このコマンドでバージョン等が出てくればOKです。
この時にパスなどに日本語が含まれるとエラーします。
コマンド:emcc -v
実行
実行するには実行したいC/C++ファイルを作成して、上でインストールしたEmscripten用のプロンプト上でコンパイルします。
C/C++ファイルの作成
今回は新しい言語に遭遇したときにお決まりとなるhelloworldを表示するプログラムを作成します。
cmain.cpp#include <stdio.h> int main() { printf("hello, world!\n"); return 0; }Emscripten用のプロンプト起動
ダウンロードしてきたemsdk-masterの下にある
emcmdprompt.bat
をダブルクリックしてEmscripten用のコマンドプロンプトを起動します。コンパイル
作成したC++ファイルのフォルダまで移動して
emcc
コマンドでコンパイルを行います。
3分ほどしてコンパイルが成功すると、今いるフォルダにa.out.js
とa.out.wasm
ができています。
コマンド:emcc cmain.cpp
※この時も場所によっては管理者による実行が必要になります。実行
a.out.js
をnodeで実行するとhello, worldが表示されていることがわかります。>node a.out.js hello, world!おわりに
Webアセンブリについて、公式の手順通りにインストールして実行してみました。
比較的簡単にできましたが、ところどころつまずく点や考える点などがありました。
次回以降はこの環境を使ってもう少し色々なことをしていこうと思います。
- 投稿日:2020-05-24T21:06:17+09:00
簡単なローカルサーバーの立て方
Javascriptを使ったホームページ制作などをしている際に、ちょっと動作を確認しようとブラウザでファイルを直接開いた場合(URLがfile:// ~ の場合など)、JSが動作しない時があります。
これは同一性ポリシーと呼ばれる制約を受けるからなのですが、私を含め多くのプログラミング初心者はこの時初めて同一性ポリシーについて知ると思うので、この場面に直面した時の「じゃあ結局どうすれば動作確認できるようになるの?」について紹介します。あくまで、初心者に対する動作確認をするための紹介であり、同一性ポリシー及びCORSについての説明はほとんどしません。(知りたい人は各自調べてください。)
対象でない人
Apacheとかnginxを自分でセットアップできる人
解決策の手順
解決方法は2つあります。
1. VSCodeの「Live Server」拡張機能を使用する
2. python3を用いてターミナルでpython -m http.server
を使用する
ここで、python3としているのは現在のpythonの標準がpython3だからで特に理由はありません。python2を使っている人は想定していません。解決策1について
これが最も簡単だと思います。私も普段はVSCodeでプログラムを書くことが多いのでチャチャっと確認したいときはこれを使用する時があります。
手順1(インストール)
VSCodeがインストールされている状態だと仮定します。
(インストールしていない人はこちら(VSCodeの公式サイト)より自身のOSにあったタイプをインストールしてください。)インストールができたら、VSCodeの拡張機能にある「Live Server」を検索してインストール
起動方法
インストール後、VSCodeを再起動して右下の「Go Live」をクリックすると自動でデフォルトブラウザが起動して表示されます。
また、一度閉じてしまっても、
http://127.0.0.1:5500/
(http://localhost:5500/
でも良い)にアクセスすることでも同様に開くことができます。(デフォルトのポートは5500番)ちなみに、この時最初に表示されるのは、VSCodeで開いているディレクトリです。
index.html
があればindex.html
が、なければディレクトリの中身が表示されます。解決策2について
Mac, LinuxなどのUnix系OSを使っている人であればpython3は標準でインストールされていると思うのでターミナルで
$ python3 -m http.serverと入力しするとターミナルのカレントディレクトリに対応した場所が開かれローカルサーバーが立ちます。
簡単に説明するとフラグ-m
はモジュールの使用を意味するので、httpパッケージのserverモジュールを使用するという意味になります。(間違ってたらごめんなさい。)もし、Python2系、WindowsOSを使用している場合は、
Homebrewもしくは公式サイト(こちら)よりインストールしてきて上記のコマンドを実行することでローカルサーバーが立ちます。終わりに
良いローカルサーバーライフを!(笑)
- 投稿日:2020-05-24T20:39:45+09:00
jQuery文法チートシート
jQueryとは
JavaScriptをより簡単に書けるように設計されたJavaScriptライブラリのこと
jQuery公式サイトHTML&CSS操作系
text
テキストの変更と取得
See the Pen
jQuery_text by engineerhikaru (@engineerhikaru)
on CodePen.
html
HTMLの変更と取得
See the Pen
jQuery_html by engineerhikaru (@engineerhikaru)
on CodePen.
prepend
要素内の先頭にHTMLを挿入
See the Pen
jQuery_prepend by engineerhikaru (@engineerhikaru)
on CodePen.
append
要素内の最後にHTMLを挿入
See the Pen
jQuery_append by engineerhikaru (@engineerhikaru)
on CodePen.
before
要素の前にHTMLを挿入
See the Pen
jQuery_before by engineerhikaru (@engineerhikaru)
on CodePen.
after
要素の後にHTMLを挿入
See the Pen
jQuery_after by engineerhikaru (@engineerhikaru)
on CodePen.
prependTo
他の要素内の先頭に要素を移動
See the Pen
jQuery_prependTo by engineerhikaru (@engineerhikaru)
on CodePen.
appendTo
他の要素内の最後に要素を移動
See the Pen
jQuery_appendTo by engineerhikaru (@engineerhikaru)
on CodePen.
insertBefore
他の要素の前に要素を移動
See the Pen
jQuery_insertBefore by engineerhikaru (@engineerhikaru)
on CodePen.
insertAfter
他の要素の後に要素を移動
See the Pen
jQuery_insertAfter by engineerhikaru (@engineerhikaru)
on CodePen.
wrap
指定した要素を他の要素で入れ子にする
See the Pen
jQuery_wrap by engineerhikaru (@engineerhikaru)
on CodePen.
wrapAll
指定した要素の要素全体を他の要素で入れ子にする
See the Pen
jQuery_wrapAll by engineerhikaru (@engineerhikaru)
on CodePen.
wrapInner
指定した要素の子要素・テキストを他の要素で入れ子にする
See the Pen
jQuery_wrapInner by engineerhikaru (@engineerhikaru)
on CodePen.
unWrap
指定した要素の親要素を除外
See the Pen
jQuery_unwrap by engineerhikaru (@engineerhikaru)
on CodePen.
replaceWith
指定した要素を他の要素に書き換える
See the Pen
jQuery_replaceWith by engineerhikaru (@engineerhikaru)
on CodePen.
remove
remove:指定した要素を削除
removeAttr:指定した要素の属性を削除
removeClass:指定した要素のクラスを削除
See the Pen
jQuery_remove by engineerhikaru (@engineerhikaru)
on CodePen.
attr
指定した属性の値を変更・取得
See the Pen
jQuery_attr by engineerhikaru (@engineerhikaru)
on CodePen.
addClass
指定した要素にクラスを追加
See the Pen
jQuery_addClass by engineerhikaru (@engineerhikaru)
on CodePen.
css
指定したCSSプロパティの値を設定・取得
See the Pen
jQuery_css by engineerhikaru (@engineerhikaru)
on CodePen.
イベント処理系
click
指定した要素のクリック時に処理を実行
See the Pen
jQuery_click by engineerhikaru (@engineerhikaru)
on CodePen.
dbclick
指定した要素のダブルクリック時に処理を実行
See the Pen
jQuery_dblclick by engineerhikaru (@engineerhikaru)
on CodePen.
hover
指定した要素のマウスオーバー/アウト時に処理を実行
See the Pen
jQuery_hover by engineerhikaru (@engineerhikaru)
on CodePen.
mouseover ・ mouseout
mouseover:指定した要素のマウスオーバー時に処理を実行
mouseout:指定した要素のマウスアウト時に処理を実行
See the Pen
jQuery_mouseover by engineerhikaru (@engineerhikaru)
on CodePen.
mousedown ・ mouseup
mousedown:指定した要素のマウスボタンが押された時に処理を実行
mouseup:指定した要素のマウスボタンが離された時に処理を実行
See the Pen
jQuery_mousedown by engineerhikaru (@engineerhikaru)
on CodePen.
mousemove
マウスが移動した時に処理を実行
See the Pen
jQuery_mousemove by engineerhikaru (@engineerhikaru)
on CodePen.
one
イベント発生時に一度だけ処理を実行
See the Pen
jQuery_one by engineerhikaru (@engineerhikaru)
on CodePen.
on
対象要素を絞ってイベントを登録
See the Pen
jQuery_on by engineerhikaru (@engineerhikaru)
on CodePen.
off
設定されているイベント処理を取り消し
See the Pen
jQuery_off by engineerhikaru (@engineerhikaru)
on CodePen.
focus ・ blur
focus:フォームにフォーカスされた時に処理を実行
blur:フォームのフォーカスが外れた時に処理を実行
See the Pen
jQuery_focus by engineerhikaru (@engineerhikaru)
on CodePen.
change
フォームの入力値が変更された時に処理を実行
See the Pen
jQuery_change by engineerhikaru (@engineerhikaru)
on CodePen.
submit
フォームの送信ボタンがクリックされた時に処理を実行
See the Pen
jQuery_submit by engineerhikaru (@engineerhikaru)
on CodePen.
アニメーション系
show ・ hide
show:要素を徐々に表示
hide:要素を徐々に非表示
See the Pen
jQuery_show by engineerhikaru (@engineerhikaru)
on CodePen.
toggle
要素を徐々に表示・非表示
See the Pen
jQuery_toggle by engineerhikaru (@engineerhikaru)
on CodePen.
slideDown ・ slideUp
slideDown:要素をスライドアニメーションで表示
slideUp:要素をスライドアニメーションで非表示
See the Pen
jQuery_slideDown-slideUp by engineerhikaru (@engineerhikaru)
on CodePen.
slideToggle
要素をスライドアニメーションで表示・非表示
See the Pen
jQuery_slideToggle by engineerhikaru (@engineerhikaru)
on CodePen.
fadeIn ・ fadeOut
fadeIn:要素をフェードインで表示
fadeOut:要素をフェードアウトで非表示
See the Pen
jQuery_fadeIn-fadeOut by engineerhikaru (@engineerhikaru)
on CodePen.
fadeTo
要素の透明度を指定した値に徐々に変更
See the Pen
jQuery_fadeTo by engineerhikaru (@engineerhikaru)
on CodePen.
animate
任意のCSSプロパティの値を徐々に変更
See the Pen
jQuery_animate by engineerhikaru (@engineerhikaru)
on CodePen.
- 投稿日:2020-05-24T20:26:38+09:00
文章中の誤字を発見するwebアプリを作った
文章中の誤字を見つけ出して修正するためのwebアプリを作った。
できたもの
https://intriguing-soda.glitch.me/
使ったもの
- Node.js + express + ejs
- A3RT Proofreading API
- glitch
気になるところ
- 正しい箇所が修正候補として指摘されてしまう場合がある(特に間違っている箇所の隣の文字)
- ejsの使用について
server.js//これがあれば app.set("view engine", "ejs"); //こっちはいらない? var ejs = require("ejs");
- 投稿日:2020-05-24T20:18:22+09:00
LinkeInのつながり申請をJavaScriptで自動化&思わぬ落とし穴
こんにちは、大学生としてプログラミングで遊んでいるKantaです!
今回はLinkedInのつながり申請のJavaScriptでの自動化について記事を書きます。なぜLinkedIn?つながりを増やす意味は?
エンジニアのみなさんはLinkedInを有効活用していらっしゃいますよね。
業界のトップに位置する人の経験や経歴、スキルを分析することで、自分がどのような方向に進んでいくべきか、何を身に付けておくべきかを考えるきっかけになります。
また、業界のポジションの種類などの知識も自然と学ぶことができます。意外とつながり申請を受け入れてくれる人は多いです。
つながりの質も長期的には重要ですが、つながりの数はある程度多ければ多いほど、リクルーターからのメッセージも増え、大学生の私でも在宅ワークやインターンの募集を受け取ることは多いです。意外な出会いをもたらしてくれるSNS、それがLinkedInです。繋がりを増やすために、知人に申請したり、憧れの企業にお勤めの方々に申請などを手動でポチポチするのは手間です。
その時間があれば、スクワットが何回できるでしょうか?魚を何匹捌けるでしょうか?そんなことを考えていると、「リンクトインの繋がり申請を自動化できないか?」
という考えが浮かんでみました。それが今回の記事を書くきっかけです。「つながり申請の自動化」具体的な操作に分解してみる
Chromeの拡張機能Tampermonkeyを用いてブラウザ上でJavaScriptを実行していきます。
具体的は操作は以下です:
- ユーザーの検索結果の画面の繋がり申請ボタンをクリックする。
- 確認ウィンドウがでてきたら、送信ボタンをクリックする。
- もし申請にメールアドレス認証などが必要であればキャンセルボタンを押す。
- ページのどの部分のユーザーに申請したかを確認する。
- もしページの最後であれば、次のページに向かう。
この1-5の流れを、繋がりボタンが存在する限り、ランダム間隔をおいてループで実行する。
大まかな自動化の流れ
さて、目的を定めたら次はChromeの開発者ツールでサイトの調査をします。
右クリックで出てくるメニューから「検証」ボタンをクリックします。これがみなさん大好き、Chrome開発者ツールですね。
Chromeの開発者ツールで画像のような四角形にカーソルが載ったアイコンをクリックすると、特定の要素のコードなどが見れます。
このアイコンをクリックした後に、ページ上の部分をクリックすると、その部分のHTML上での場所など、様々な情報が見れます。要素の特定
さて、JavaScriptでDOMに変更を加えるには、目的の要素を一意に特定する必要があります。
目的の要素を一意に特定する手段としてid,class,属性があります。下の画像のように、選択した要素のJS Pathをコピーして利用するという手もありますが、「あるタグの何番目の子要素」みたいな特定方法なので汎用性に欠けます。また、HTMLの構造の変更に影響を受けやすいです。特に自動生成されているサイトや、変更が著しいサイトではお勧めできません。
よって、一貫した要素の特定をするためには、id, class, タグの属性,クエリセレクタなどを用いるのが最適です。
WEBサイトの作成者も、CSSなどで装飾する際に、分かりやすい命名法をしてくれます。
そうした命名から、要素の機能を推定できます。例えば、LinkedInのつながり申請ボタンの部分のHTML要素を覗いてみると次のようになっています。
(注意:一部省略してます。)<button data-control-name="srp_profile_actions">省略..</button>属性がこのように与えられていると、サイトの設計者だけでなく、自動化ツールを作る我々も恩恵を受けることができます。
このようにして、classや属性のなかで、機能が推定しやすく変更の影響を受けにくいものを選んで、クエリセレクタでHTMLcollextionを取得します。
これはHTMLの要素の参照からなる配列のようなものです。特定した要素をJavaScriptで操作する(DOM操作)
要素が特定できたら、JavaScriptのおなじみのDOM操作をしていきましょう。
上の例のボタンを操作するためには、まずこうします。var sendInvitationElements=document.querySelectorAll('button[data-control-name="srp_profile_actions"]');こうして各要素に対しておなじみのdocumentオブジェクトのメソッドやプロパティがつかえます。
例えば、上で取得した要素たちのうち、i番目の要素をクリックしたことにするには、sendInvitationElements[i].click();のようなコードを書けばいいですね。このような感じでHTMLの一部の中身を取得したり、クリックしたり、ループしたり、条件分岐の処理を書いていけば案外すぐに自動化は可能です。
このようにして、自動化のスクリプトを書いていきましょう。完成したもの "LinkedInAutoConnector"
コード:
https://github.com/kantasv/LinkedInAutoConnector/blob/master/linkedInAutoConnector.jsまず、拡張機能を有効にした後、キーワードで検索し、その後"People"の欄をクリックすると繋がり申請が自動で始まります。
一応、「もう十分」となったときのために、画面左下にボタンを用意しています。
これをクリックすることで一時的につながり申請が止まります。
これで、つながりを増やしまくるぜ!と怒涛のつながり申請をしまくると意気込んでいた筆者ですが、世の中はそんなに甘くありませんでした。
正確には覚えていませんが、二百人くらいはつながりが増えたところで、思わぬ落とし穴にはまりました。落とし穴:LinkedInで自動化ツールの利用は利用規約違反
アカウント一時停止
ある朝、LinkedInにアクセスすると、ログインができません。
眠い目をこすりながら、メッセージを見ると、アカウントが一時的に停止されたとのメッセージに気付きました。
めちゃめちゃ朝から焦りました。
本人確認が必要とのことで、カスタマーサポートとやりとりを行いました。LinkedInの利用規約には・・・
アカウントが停止された背景について詳しく調べてみると・・・
次のページに該当するように、LinkedInでの各種自動化ツールの利用は利用規約違反だったのです。
https://www.linkedin.com/help/linkedin/answer/60453/-?lang=ja"ボットやその他の自動化された方法を使用して、弊社のサービスしたり、連絡先を追加/ダウンロードしたり、メッセージを送信またはリダイレクトすること"
この部分が該当していました。本人確認によりアカウント復活
自分のパスポートの画像を送信することで、アカウントは復活しました。
このような出来事は人生で初めてだったので、正直焦りました。。。まとめ
自動化ツールは便利なので、自粛期間中にみなさんも量産してみてはいかがでしょうか。
ただ、そうしたツールの利用を許可しないWEBサイトも少なからず存在します。
また、こうしたツールは一歩間違えればDDoS Attackのようにサーバーに負荷をかけてしまうことも考えれます。
たとえ自動操作の間隔をランダムにしたり、回りくどい回避策をとったとして、いつかは対策されますし、ばれます。
節度をもって、自動化で毎日をより充実させることができるといいですね。
それでは、ここまで読んでいただきありがとうございました。コメント欄に、あなたが最近した自動化、これからしたい自動化、あったらいいなこんな自動化など書き込んでください!
楽しみにしています!どのようにしたら自動化できるかということに「純粋な知的好奇心」があるみなさんに最後にソースコードをここにも残しておきます。
GitHubにも挙げてます。
https://github.com/kantasv/LinkedInAutoConnector/blob/master/linkedInAutoConnector.js| (function () { | |:--| | 'use strict'; | | | | var pageIndex; | | var setIntervalId; | | | | var isCuurentPageSearchResultPage = () => { | | return window.location.href.toLocaleLowerCase().indexOf('search/results/people/') != 1 | | } | | | | | | //detects page result index when current page is clearly the people search result page | | if (isCuurentPageSearchResultPage()) { | | window.location.href.split('&').forEach(elm => { | | if (elm.indexOf('page=') != -1) { | | var start = 'page='.length | | pageIndex = parseInt(elm.slice(start)) | | console.log('pageIndex', pageIndex) | | } | | }) | | //when could not "page=" but clearly the current page is clearly the people search result page | | if (!pageIndex) { | | window.location.href += '&page=1' | | } | | | | } | | | | var initAutoConnector = () => { | | var connectButtonCandidates = document.querySelectorAll('button[data-control-name="srp_profile_actions"]'); | | var connectButtons = []; | | | | //filter buttons: get only connect buttons | | for (var i = 0; i < connectButtonCandidates.length; i++) { | | //console.log('Button condition', connectButtonCandidates[i].innerText) | | if (connectButtonCandidates[i].innerText == 'Connect') { connectButtons.push(connectButtonCandidates[i]) }; | | } | | | | | | //gets random integers ranged from 0 to 300 | | //by changing intervals, LinkedIn is not likely to detect this sort of automation | | var getRandomInteger = () => { | | var min = 0; | | var max = 300; | | return Math.floor(Math.random() * (max + 1 - min)) + min; | | } | | | | var connectButtonCount = 0 | | | | var connectButtonOperation = () => { | | | | if (connectButtonCount < connectButtons.length) { | | | | //clicks "Connect button" | | connectButtons[connectButtonCount].click() | | //clicks "Send Invitation" button | | setTimeout(() => { | | //clicks only when "Send Invitation" exists | | var sendInvitationElement = document.querySelector('button[aria-label="Send invitation"]') | | var verificationOperationDismissElement = document.querySelector('button[aria-label="Dismiss"]') | | if (sendInvitationElement) { | | console.log('sendInvitationElement exists', sendInvitationElement) | | sendInvitationElement.click() | | } else if (verificationOperationDismissElement) { | | console.log(verificationOperationDismissElement) | | //cancels when you face "verify" dialog | | verificationOperationDismissElement.click() | | } else { | | console.log('Unexpected error. Could not find button elements for operations.') | | } | | connectButtonCount++ | | }, getRandomInteger()) | | | | | | } else { | | console.log('already clicked all connect buttons') | | clearInterval(setIntervalId) | | //when no connect buttons available | | //then move to next result page | | var currentUrl = window.location.href | | console.log(`page=${pageIndex}`, '->', `page=${pageIndex + 1}`) | | var nextPageUrl = currentUrl.replace(`page=${pageIndex}`, `page=${pageIndex + 1}`) | | //console.log(currentUrl.slice(50),nextPageUrl.slice(50)) | | setTimeout(() => { window.location.href = nextPageUrl }, 1000) | | | | } | | | | } | | | | setIntervalId = setInterval(connectButtonOperation, 1000 + getRandomInteger()) | | | | } | | | | | | //checks if the current page is the search result page | | if (isCuurentPageSearchResultPage()) { | | | | console.log('Found "Connect" buttons. Startsing automatically conectiong...') | | window.onload = () => { initAutoConnector() } | | | | } else { | | console.log('Could not find "Connect" buttons. This page may not be search result page.') | | } | | | | | | //adds Autoconnector bar element to DOM | | var initACBar = () => { | | | | var autoconnectStopButton = document.createElement('div'); | | autoconnectStopButton.innerHTML = `<span id='ACstatus'>Sending invitations automatically...</span><p id='ACstopButton'>Click here to stop LinkedIn Autoconector temporalily</p> | | <h6>LinkedIn Autoconnector - Powered by Kanta Yamaoka.</h6>` | | | | | | var css = (prop, value) => { | | autoconnectStopButton.style[prop] = value | | } | | | | css('width', '30%') | | css('height', '100px') | | css('backgroundColor', 'white') | | css('color', '#0178B5') | | css('border', '2px solid #0178B5') | | css('borderRadius', '10px') | | css('textAlign', 'center') | | css('textHeight', '10px') | | css('position', 'fixed') | | css('bottom', '10%') | | css('left', '10%') | | css('zIndex', '10000') | | | | | | | | document.body.appendChild(autoconnectStopButton) | | document.getElementById('ACstopButton').style.margin = '10px' | | | | autoconnectStopButton.onclick = () => { | | clearInterval(setIntervalId) | | css('backgroundColor', '#c8c8c8') | | document.getElementById('ACstatus').innerText = 'Autoconnector temporalily disabled.' | | document.getElementById('ACstopButton').innerText = 'To use Autoconecttor again, please refresh the page.' | | | | } | | | | | | } | | | | initACBar() | | | | })(); |
- 投稿日:2020-05-24T20:18:22+09:00
LinkedInのつながり申請をJavaScriptで自動化&思わぬ落とし穴
こんにちは、大学生としてプログラミングで遊んでいるKantaです!
今回はLinkedInのつながり申請のJavaScriptでの自動化について記事を書きます。なぜLinkedIn?つながりを増やす意味は?
エンジニアのみなさんはLinkedInを有効活用していらっしゃいますよね。
業界のトップに位置する人の経験や経歴、スキルを分析することで、自分がどのような方向に進んでいくべきか、何を身に付けておくべきかを考えるきっかけになります。
また、業界のポジションの種類などの知識も自然と学ぶことができます。意外とつながり申請を受け入れてくれる人は多いです。
つながりの質も長期的には重要ですが、つながりの数はある程度多ければ多いほど、リクルーターからのメッセージも増え、大学生の私でも在宅ワークやインターンの募集を受け取ることは多いです。意外な出会いをもたらしてくれるSNS、それがLinkedInです。ただ、繋がりを増やすために、知人に申請したり、憧れの企業にお勤めの方々に申請などを手動でポチポチするのは手間です。
また、日本の大学生の知人はそもそもそんなにLinkedInやってません・・。
その時間があれば、スクワットが何回できるでしょうか?魚を何匹捌けるでしょうか?そんなことを考えていると、「リンクトインの繋がり申請をターゲットを定めて自動化できないか?」
という考えが浮かんでみました。それが今回の記事を書くきっかけです。「つながり申請の自動化」具体的な操作に分解してみる
Chromeの拡張機能Tampermonkeyを用いてブラウザ上でJavaScriptを実行していきます。
具体的は操作は以下です:
- ユーザーの検索結果の画面の繋がり申請ボタンをクリックする。
- 確認ウィンドウがでてきたら、送信ボタンをクリックする。
- もし申請にメールアドレス認証などが必要であればキャンセルボタンを押す。
- ページのどの部分のユーザーに申請したかを確認する。
- もしページの最後であれば、次のページに向かう。
この1-5の流れを、繋がりボタンが存在する限り、ランダム間隔をおいてループで実行する。
大まかな自動化の流れ
さて、目的を定めたら次はChromeの開発者ツールでサイトの調査をします。
右クリックで出てくるメニューから「検証」ボタンをクリックします。これがみなさん大好き、Chrome開発者ツールですね。
Chromeの開発者ツールで画像のような四角形にカーソルが載ったアイコンをクリックすると、特定の要素のコードなどが見れます。
このアイコンをクリックした後に、ページ上の部分をクリックすると、その部分のHTML上での場所など、様々な情報が見れます。要素の特定
さて、JavaScriptでDOMに変更を加えるには、目的の要素を一意に特定する必要があります。
目的の要素を一意に特定する手段としてid,class,属性があります。下の画像のように、選択した要素のJS Pathをコピーして利用するという手もありますが、「あるタグの何番目の子要素」みたいな特定方法なので汎用性に欠けます。また、HTMLの構造の変更に影響を受けやすいです。特に自動生成されているサイトや、変更が著しいサイトではお勧めできません。
よって、一貫した要素の特定をするためには、id, class, タグの属性,クエリセレクタなどを用いるのが最適です。
WEBサイトの作成者も、CSSなどで装飾する際に、分かりやすい命名法をしてくれます。
そうした命名から、要素の機能を推定できます。例えば、LinkedInのつながり申請ボタンの部分のHTML要素を覗いてみると次のようになっています。
(注意:一部省略してます。)<button data-control-name="srp_profile_actions">省略..</button>属性がこのように与えられていると、サイトの設計者だけでなく、自動化ツールを作る我々も恩恵を受けることができます。
このようにして、classや属性のなかで、機能が推定しやすく変更の影響を受けにくいものを選んで、クエリセレクタでHTMLcollextionを取得します。
これはHTMLの要素の参照からなる配列のようなものです。特定した要素をJavaScriptで操作する(DOM操作)
要素が特定できたら、JavaScriptのおなじみのDOM操作をしていきましょう。
上の例のボタンを操作するためには、まずこうします。var sendInvitationElements=document.querySelectorAll('button[data-control-name="srp_profile_actions"]');こうして各要素に対しておなじみのdocumentオブジェクトのメソッドやプロパティがつかえます。
例えば、上で取得した要素たちのうち、i番目の要素をクリックしたことにするには、sendInvitationElements[i].click();のようなコードを書けばいいですね。このような感じでHTMLの一部の中身を取得したり、クリックしたり、ループしたり、条件分岐の処理を書いていけば案外すぐに自動化は可能です。
このようにして、自動化のスクリプトを書いていきましょう。完成したもの "LinkedInAutoConnector"
コード:
https://github.com/kantasv/LinkedInAutoConnector/blob/master/linkedInAutoConnector.jsこれをChrome拡張機能のTampermonkeyに入れれば、"LinkedInAutoConnector"が実行できます。
まず、拡張機能を有効にした後、キーワードで検索し、その後"People"の欄をクリックすると繋がり申請が自動で始まります。
一応、「もう十分」となったときのために、画面左下にボタンを用意しています。
これをクリックすることで一時的につながり申請が止まります。
これで、つながりを増やしまくるぜ!と怒涛のつながり申請をしまくると意気込んでいた筆者ですが、世の中はそんなに甘くありませんでした。
正確には覚えていませんが、二百人くらいはつながりが増えたところで、思わぬ落とし穴にはまりました。落とし穴:LinkedInで自動化ツールの利用は利用規約違反
アカウント一時停止
ある朝、LinkedInにアクセスすると、ログインができません。
眠い目をこすりながら、メッセージを見ると、アカウントが一時的に停止されたとのメッセージに気付きました。
めちゃめちゃ朝から焦りました。
本人確認が必要とのことで、カスタマーサポートとやりとりを行いました。LinkedInの利用規約には・・・
アカウントが停止された背景について詳しく調べてみると・・・
次のページに該当するように、LinkedInでの各種自動化ツールの利用は利用規約違反だったのです。
https://www.linkedin.com/help/linkedin/answer/60453/-?lang=ja"ボットやその他の自動化された方法を使用して、弊社のサービスしたり、連絡先を追加/ダウンロードしたり、メッセージを送信またはリダイレクトすること"
この部分が該当していました。本人確認によりアカウント復活
自分のパスポートの画像を送信することで、アカウントは復活しました。
このような出来事は人生で初めてだったので、正直焦りました。。。まとめ
自動化ツールは便利なので、自粛期間中にみなさんも量産してみてはいかがでしょうか。
ただ、そうしたツールの利用を許可しないWEBサイトも少なからず存在します。
また、こうしたツールは一歩間違えればDDoS Attackのようにサーバーに負荷をかけてしまうことも考えれます。
たとえ自動操作の間隔をランダムにしたり、回りくどい回避策をとったとして、いつかは対策されますし、ばれます。
節度をもって、自動化で毎日をより充実させることができるといいですね。
それでは、ここまで読んでいただきありがとうございました。コメント欄に、あなたが最近した自動化、これからしたい自動化、あったらいいなこんな自動化など書き込んでください!
楽しみにしています!どのようにしたら自動化できるかということに「純粋な知的好奇心」があるみなさんに最後にソースコードをここにも残しておきます。
GitHubにも挙げてます。
https://github.com/kantasv/LinkedInAutoConnector/blob/master/linkedInAutoConnector.js| (function () { | |:--| | 'use strict'; | | | | var pageIndex; | | var setIntervalId; | | | | var isCuurentPageSearchResultPage = () => { | | return window.location.href.toLocaleLowerCase().indexOf('search/results/people/') != 1 | | } | | | | | | //detects page result index when current page is clearly the people search result page | | if (isCuurentPageSearchResultPage()) { | | window.location.href.split('&').forEach(elm => { | | if (elm.indexOf('page=') != -1) { | | var start = 'page='.length | | pageIndex = parseInt(elm.slice(start)) | | console.log('pageIndex', pageIndex) | | } | | }) | | //when could not "page=" but clearly the current page is clearly the people search result page | | if (!pageIndex) { | | window.location.href += '&page=1' | | } | | | | } | | | | var initAutoConnector = () => { | | var connectButtonCandidates = document.querySelectorAll('button[data-control-name="srp_profile_actions"]'); | | var connectButtons = []; | | | | //filter buttons: get only connect buttons | | for (var i = 0; i < connectButtonCandidates.length; i++) { | | //console.log('Button condition', connectButtonCandidates[i].innerText) | | if (connectButtonCandidates[i].innerText == 'Connect') { connectButtons.push(connectButtonCandidates[i]) }; | | } | | | | | | //gets random integers ranged from 0 to 300 | | //by changing intervals, LinkedIn is not likely to detect this sort of automation | | var getRandomInteger = () => { | | var min = 0; | | var max = 300; | | return Math.floor(Math.random() * (max + 1 - min)) + min; | | } | | | | var connectButtonCount = 0 | | | | var connectButtonOperation = () => { | | | | if (connectButtonCount < connectButtons.length) { | | | | //clicks "Connect button" | | connectButtons[connectButtonCount].click() | | //clicks "Send Invitation" button | | setTimeout(() => { | | //clicks only when "Send Invitation" exists | | var sendInvitationElement = document.querySelector('button[aria-label="Send invitation"]') | | var verificationOperationDismissElement = document.querySelector('button[aria-label="Dismiss"]') | | if (sendInvitationElement) { | | console.log('sendInvitationElement exists', sendInvitationElement) | | sendInvitationElement.click() | | } else if (verificationOperationDismissElement) { | | console.log(verificationOperationDismissElement) | | //cancels when you face "verify" dialog | | verificationOperationDismissElement.click() | | } else { | | console.log('Unexpected error. Could not find button elements for operations.') | | } | | connectButtonCount++ | | }, getRandomInteger()) | | | | | | } else { | | console.log('already clicked all connect buttons') | | clearInterval(setIntervalId) | | //when no connect buttons available | | //then move to next result page | | var currentUrl = window.location.href | | console.log(`page=${pageIndex}`, '->', `page=${pageIndex + 1}`) | | var nextPageUrl = currentUrl.replace(`page=${pageIndex}`, `page=${pageIndex + 1}`) | | //console.log(currentUrl.slice(50),nextPageUrl.slice(50)) | | setTimeout(() => { window.location.href = nextPageUrl }, 1000) | | | | } | | | | } | | | | setIntervalId = setInterval(connectButtonOperation, 1000 + getRandomInteger()) | | | | } | | | | | | //checks if the current page is the search result page | | if (isCuurentPageSearchResultPage()) { | | | | console.log('Found "Connect" buttons. Startsing automatically conectiong...') | | window.onload = () => { initAutoConnector() } | | | | } else { | | console.log('Could not find "Connect" buttons. This page may not be search result page.') | | } | | | | | | //adds Autoconnector bar element to DOM | | var initACBar = () => { | | | | var autoconnectStopButton = document.createElement('div'); | | autoconnectStopButton.innerHTML = `<span id='ACstatus'>Sending invitations automatically...</span><p id='ACstopButton'>Click here to stop LinkedIn Autoconector temporalily</p> | | <h6>LinkedIn Autoconnector - Powered by Kanta Yamaoka.</h6>` | | | | | | var css = (prop, value) => { | | autoconnectStopButton.style[prop] = value | | } | | | | css('width', '30%') | | css('height', '100px') | | css('backgroundColor', 'white') | | css('color', '#0178B5') | | css('border', '2px solid #0178B5') | | css('borderRadius', '10px') | | css('textAlign', 'center') | | css('textHeight', '10px') | | css('position', 'fixed') | | css('bottom', '10%') | | css('left', '10%') | | css('zIndex', '10000') | | | | | | | | document.body.appendChild(autoconnectStopButton) | | document.getElementById('ACstopButton').style.margin = '10px' | | | | autoconnectStopButton.onclick = () => { | | clearInterval(setIntervalId) | | css('backgroundColor', '#c8c8c8') | | document.getElementById('ACstatus').innerText = 'Autoconnector temporalily disabled.' | | document.getElementById('ACstopButton').innerText = 'To use Autoconecttor again, please refresh the page.' | | | | } | | | | | | } | | | | initACBar() | | | | })(); |
- 投稿日:2020-05-24T19:51:41+09:00
noteのタイトルとテキストをコピーするブックマークレット
noteにエキスポート機能がなかったので、
簡易的にタイトルとテキストをコピーするブックマークレットを作りました。javascript:(function(){var elements = document.getElementsByClassName("o-noteContentText");var text = elements[0];var range = document.createRange();range.selectNodeContents(text);window.getSelection().addRange(range);document.execCommand("copy");})();↑のコードをコピーして、
適当なページをブックマークに追加→編集で、「URL」に貼り付けて下さい。「名前」は任意で。補足:コードの解説
noteのタイトルと文章部分が
o-noteContentText
というclass名で囲われていたので、
そのノードを取得、選択してコピーするという単純な構造です。なので、class名が変わるような更新があると使えなくなります><
javascript:( function(){ var elements = document.getElementsByClassName("o-noteContentText"); var text = elements[0]; var range = document.createRange(); range.selectNodeContents(text); window.getSelection().addRange(range); document.execCommand("copy"); } )();理想をいえば、複数ページを一気にコピーできるとこまで、できれば良かったのですが、
すみません、力不足でした…お手数ですが、作成したページを一つずつ開いて、お使いくださいm(_ _)m
- 投稿日:2020-05-24T18:47:13+09:00
express-generatorで開発の雛形をサクッと作ってみたときのメモ
きょうぷろぐらみんぐをはじめたました!(大嘘)
webpackあたりまだ良くわかっていない人です。
今回、express-generarotを使って環境?を作ったときに知ったこととかをメモしていく。
メモだよ。
必要があればコメントでメモ修正していただけるとありがたき幸せ。express-generatorの巻
expressで環境を構築するときに
express-generator
というものを使うと構造をぱぱっと作ってくれる。
そのためにnpm install express-generator -gをする。
-g
でいれるよ。グローバルインストールするのが重要。たぶん。あとは
express hogeでサクッと構造を作る。
色々オプションつけられるよ。
express --view pug --css sass hogeみたな。
--view pug
とかはexpress -h
に書いてあるから見てね。(ちょっと脱線)package.jsonの巻
これまでテキトーに
npm install
みたいなのをしていたが、ESlintとかそういうのを導入しているときにnpm install -D
みたいなことを書いているサイトがあったから気になって調べてみた。そしたら、実は開発用のパッケージとアプリの動作用のパッケージで記述される場所が違ったらしい。ほぇ〜。
下のを読んだ。
参考:https://qiita.com/chihiro/items/ca1529f9b3d016af53ecアプリの動作に必要なものは
dependencies
npm install --save hoge開発に必要なものは
devDependencies
npm install --save-dev hogeとしていれるんだと。
で実際に
package.json
を使ってパッケージ入れるときは
package.json
に書かれているすべてのパッケージをインストールするnpm install
package.json
のdependencies
に書かれているパッケージのみインストールするnpm install --productionだから、開発に必要なパッケージは
npm install --save-dev eslintで書かなくちゃいけないんだな。
ESlintとかはそうだね。ちなみに
npm 5.0.0
以降は--save
はつけなくてOK。npm v5以降↓↓
参考:https://docs.npmjs.com/cli/install
アプリの動作に必要なものは
dependencies
npm install hoge開発に必要なものは
devDependencies
npm install -D hogeESLintの巻
ESLintってのを入れると、書いているコードを既存のルールに当てはめて勝手にきれいにしてくれるのでめちゃくちゃおすすめ。
当然入れる。ESLintのは開発でしか使わないので
npm install -D eslintこれでESLintを使える。
初期設定は
eslint --init
なんだけど、グローバルインストールしていないから./node_modules/.bin/eslint --initでESlintの初期設定を行う。
いろいろ選べば勝手に.eslintrc.json
みたいなのを作ってくれる。? How would you like to use ESLint? (Use arrow keys) To check syntax only ❯ To check syntax and find problems To check syntax, find problems, and enforce code style上みたい感じなのが出てくるからいい感じに選んでくれ。
nodemonの巻
コードを更新するたびに
Ctrl + c
するのがだるいのでnodemonを使う。
これは開発するときだけ使うのでnpm install -D nodemonだね。
さて、
package.json
のscriptを変更する必要があります。"scripts": { "start": "node ./bin/www" },こうなっていると思うから
"scripts": { "start": "nodemon ./bin/www" },とします。
これでコード編集されると自動で監視してくれるようになるよ。わーい。
以上?
やったのはこんだけ。
あとは普通にexpress通りに作った。調べてたらES6を使うためにbabelを入れようみたいな記事があったが、上の設定だけでアロー関数使えたよ。よくわからん。
だれか教えてTT以上です。
- 投稿日:2020-05-24T18:35:39+09:00
Javacript Dateメソッド わかりやすく日時を表示する
Dateオブジェクト
1現在の日時を取得する
2過去や未来の日時を指定する
3日時の計算をする①Dateオブジェクトを使う際は初期化をする
const now = new Date();newはオブジェクトを初期化するためのキーワード。
現在日時を記憶した状態でDateオブジェクトを初期化するnew Date();②年、月、日などを個別に取得する
const now = new Date(); const year = now.getFullYear(); const month = now.getMonth(); const date = now.getDate(); const hour = now.getHours(); const min = now.getMinutes();③取得したらあとは出力するだけ
const output = `${year}/${month + 1}/${date} ${hour}:${min}`; document.getElementById('time').textContent = output;*now.getMonth・・・実際の月-1になるため+1しないといけない
12時間時計にしてみよう
① ampmを定義し、空の文字列(文字列が0個の文字列)を代入して準備する
let ampm = '';②午前中、午後に場合わけ
let ampm =''; if(hour < 12){ ampm ='a.m.'; } else{ ampm = 'p.m.' }③24時間→12時間に変える
const output = `${year}/${month + 1}/${date} ${hour%12}:${min}${ampm}`;
- 投稿日:2020-05-24T18:35:25+09:00
変数参照の遅延
解決
グローバルスコープでも以下のように修正して解決しました。
"/3[0-9]/g" → "/3[0-9]/"で解決しました。問題
変数regをグローバルスコープに設定
sample.jslet reg = /3[0-9]/g; for(let i = 1; i < 101; i++) { if(reg.test(i)) { console.log(i + "は10の位が30の数字だよ!!"); } else if(i % 3 == 0 ) { console.log(i + "は三で割り切れるよ!!"); }else { console.log(i); } }結果
30は10の位が30の数字だよ!! 31 32は10の位が30の数字だよ!! 33は三で割り切れるよ!! 34は10の位が30の数字だよ!! 35 36は10の位が30の数字だよ!! 37 38は10の位が30の数字だよ!! 39は三で割り切れるよ!!ローカル変数iの10の位が30の時に所々、期待した出力値が出ない。
例( i = 31の時"31は10の位が30の数字だよ!!"と出力されて欲しいが"31"と出力される。)原因はforループの処理速度にによるグローバル変数regを参照する速度が追いついていないのか?
と考えたので変数regをブロックスコープに設定。
※javascriptには"スコープチェーン"と言う概念がある。
この概念によると、javascriptは狭いスコープに位置する変数を優先して参照するらしい。sample.jsfor(let i = 1; i < 101; i++) { let reg = /3[0-9]/g; if(reg.test(i)) { console.log(i + "は10の位が30の数字だよ!!"); } else if(i % 3 == 0 ) { console.log(i + "は三で割り切れるよ!!"); }else { console.log(i); } }結果
30は10の位が30の数字だよ!! 31は10の位が30の数字だよ!! 32は10の位が30の数字だよ!! 33は10の位が30の数字だよ!! 34は10の位が30の数字だよ!! 35は10の位が30の数字だよ!! 36は10の位が30の数字だよ!! 37は10の位が30の数字だよ!! 38は10の位が30の数字だよ!! 39は10の位が30の数字だよ!!PS
やはり変数の参照速度の遅延が原因なのか??
わかる方、是非ご教授ください。
- 投稿日:2020-05-24T18:32:50+09:00
JavaScriptでのDOMを使って色々な操作をしてみた
作業前のウェブページ
DOMの置換をしてみた
innerHTMLを使用しましょう。
これを使うとHTMLの要素を書き換えることができます。
では早速コードです。
以下はscript.jsでの書き込みです。window.addEventListener("load", function() { let btn = document.querySelector("a#btn-square"); btn.addEventListener("click", function() { console.log("Hello world"); }); 以下、置換をするために追加したコードです let btn2 = document.querySelector("a#btn-square2"); let changeText = document.querySelector("p.text2"); btn2.addEventListener("click", function() { changeText.innerHTML = '変更されました'; }); });以下、HTMLです。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>JavaScriptPractice</title> <link rel="stylesheet" type="text/css" href="style.css"> <script src="script.js"></script> </head> <body> <div class = "top"> <a href="#" id="btn-square">おはよう</a> <p class = "text">挨拶は大事だよ</p> </div> <div class = "middle"> <a href="#" id="btn-square2">こんにちは</a> <p class = "text2">挨拶は基本だよ</p> </div> <div class = "down"> <a href="#" id="btn-square3">おやすみ</a> </div> </body> </html>やっていることは単純です。
まず、document.querySelector("a#btn-square2");で
要素を取得しています。そしてそれを変数で定義しています。
let changeText = document.querySelector("p.text2");でも
要素を取得しています。同じように変数で定義をしています。
更に以下のコードがありましたね。btn2.addEventListener("click", function() { changeText.innerHTML = '変更されました'; });このコードではクリックしたときに変数(changeText)にあるものをinnerHTMLメソッドを使って変更をさせるようにしています。
その結果、置換ボタンを押すと以下のようになります。
DOMの追加をしてみる
今度はクリックすると要素が追加される機能を実装しようと思います。
それにはclassList.addメソッドを用います。
では実際のコードです。window.addEventListener("load", function() { let btn = document.querySelector("a#btn-square"); btn.addEventListener("click", function() { console.log("Hello world"); }); let btn2 = document.querySelector("a#btn-square2"); let changeText = document.querySelector("p.text2"); btn2.addEventListener("click", function() { changeText.innerHTML = '変更されました'; }); let btn3 = document.querySelector("a#btn-square3"); btn3.addEventListener("click", function() { changeText.classList.add("red"); }); });これとは別にCSSファイルにredクラスの中にbackground-color:red;を
記述する必要性があります。これもやっていることは凄く単純で、
document.querySelector("a#btn-square3");で要素を取得しています。
変数で定義し、その変数にaddEventListenerメソッドを用いて
イベントを発火させようとしています。
更に、置換の際に用いた変数changeTextにclassList.addメソッドを用いて
変数にredというクラスを加えるよう指示します。
これで加えるボタンを押すと以下のようになるはずです。
DOMの削除をしてみる
今度はすでにあるクラスを削除する実装をしていこうと思います。
それにはclassList.removeメソッドを用います。
では実際のコードです。window.addEventListener("load", function() { let btn = document.querySelector("a#btn-square"); btn.addEventListener("click", function() { console.log("Hello world"); }); let btn2 = document.querySelector("a#btn-square2"); let changeText = document.querySelector("p.text2"); btn2.addEventListener("click", function() { changeText.innerHTML = '変更されました'; }); let btn3 = document.querySelector("a#btn-square3"); btn3.addEventListener("click", function() { changeText.classList.add("red"); }); let btn4 = document.querySelector("a#btn-square4"); btn4.addEventListener("click", function(){ changeText.classList.remove("red") }) });これも単純です。
document.querySelector("a#btn-square4");で要素を取得します
そしてbtn4.addEventListenerでイベント発火準備OKです。
changeText.classList.remove("red")でredクラスを消すイベントを指示しています。
つまり、先ほどみたく赤色になった状態で削除ボタンを押すと赤色だけが消えます。
試してみてください。
遊んでみると理解が深まるのでお勧めです。
- 投稿日:2020-05-24T18:05:31+09:00
FireStoreの基本と操作 - データのCRUD操作とクエリ
Cloud FireStoreとは
Cloud FireStore(以下FireStore)とは、FireBaseの提供するドキュメント指向型
のNoSQLデータベースです。
NoSQLとしての特徴としてのスキーマレス、スケーラブルといった特徴のほかにリアルタイムアップデート、セキュリティルール、オフラインサポートといった独自の特徴を備えており、特にバックエンドを介さずにクライントサイドから直接操作できるという点が大きなポイントです。
また、β版から正式リリースされたのが2019年2月ということもあり、比較的新しい技術です。RealTimeDatabaseとの違い
FireStoreの登場時期がつい最近ということに触れましたが、FireStoreが登場する以前はRealTimeDataBaseが使用されていました。FireStoreは、RealTimeDatabaseの特徴を受け継いだデータベースであり、RealTiemDatabaseの弱点であったデータモデルを改善したりクエリを強化したりなどより使いやすくなっています。ですから、新たにプロジェクトを開始する場合にはほとんどの場合FireStoreを利用するべきです。
以下は、FireStoreとRealTimeDatabaseの比較です。
RealTimeDatabase FireStore データモデル JSON クエリ ・制限あり(フィルタリングと並び替えをの両方を同時に行うことはできない)・取得したデータのすべての子ノードを返す・JSONツリーの個々のノードまでアクセスできる・インデックスを必要としないが、データセットが大きくなるにつれて特定のクエリのパフォーマンスは低下する・ セキュリティルール 読み込みルールと書き込みルールはカスケード式に適応される スケーラビリティ スケーリングにはシャーディングが必要 課金 帯域幅とストレージにのみ課金され、課金レートは高くなる FireStoreのデータモデル
FireStoreは、
MySQL
やPostgreSQL
などのSQLデータベースと違い、「テーブル」や「行」はありません。代わりに、データはドキュメントに格納され、それがコレクションとしてまとめられています。ドキュメント
ドキュメントは、JSONとよく似たデータ構造です。
ブール値、文字列、数値、タイムスタンプ、配列、マップなどのなどのデータ型を持つ値を、キーバリューによってデータを保存します。
例えば、ユーザーを表すドキュメントは次のようになります。name: { firstName: '鈴木', lastName: '太郎', }, sex: 'male', birthDay: 847694648, favoriteFoods: ['寿司', 'ラーメン', '焼き肉']また、ドキュメントはスキーマレスであるため例えば同じユーザーを表すコレクションの中でも異なるデータ構造をもたせることができます。
name: '佐々木寿人', birthDay: 847694648, favoriteSongs: ['pretender', '紅蓮華', 'マリーゴールド']ただし、あまりに自由なデータ構造をもたせるとアプリケーションで扱いにくいデータになってしまうので、スキーマを定義した上で使用するのが一般的です。スキーマレスな構造は、例えばレガシーなデータと互換性をもたせるために使われることがあります。
また、ドキュメントのデータサイズには制約があり、ドキュメント1剣あたりのサイズが1MBまでに制限されています。
ドキュメントはアプリケーションでそのまま扱えるように設計するのがポイントとなります。
コレクション
コレクションはドキュメントを格納するコンテナであり、すべてのドキュメントはコレクションの中に保存されます。
例えば、さきほどのドキュメントはUsers
コレクションに格納されることになります。コレクション内のドキュメントの名前は一意である必要があり、独自のキーをしていするかFirestoreで自動的にランダムなIDを振り分けることになります。
リファレンス
リファレンスは、ドキュメントが格納されているパスを表現するモデルで、データベースの場所によって一意に識別されます。
例えば、先程のUsers
コレクションにアクセスするためには、次のようなレファレンスを作成します。
users/jkfjakdfjaffahi@a
users/ahjioghja@gihjafu
また、リファレンスはそのままFirestoreにデータとして保存することができます。リファレンス型のデータの保存は、ドキュメント間の関係を表現する方法として利用されます。
サブコレクション
ドキュメントの階層構造を作るために、サブコレクションを利用することができます。
サブコレクションはドキュメントの中にさらにコレクションを持つという構造になっており、ルートコレクションから見るとコレクション/ドキュメント/コレクション/ドキュメント
といった構造になります。
サブコレクションはドキュメントの親子関係、所有/被所有を表現するためにしようされ、例えばユーザー(Usres)
と記事(Articles)
の関係は次のようになります。users jkfjakdfjaffahi@a name: '鈴木太郎' sex: 'male', articles fjlkafakjfafflakju title: '記事1' body: 'ここに内容が入ります' published: false, createdAt: 1560000000 ahjioghja@gihjafu name: '佐々木寿人' sex: 'male', articles lkafhja;kfhahgi title: '記事2' body: 'あいうえお' published: true, createdAt: 1460000000リファレンスは次のように表します。
users/fjlkafakjfafflakju/articles/fjlkafakjfafflakju
なぜサブコレクションを利用するのか
このデータ構造を見て、こんなふうに思った方もいるのではないのでしょうか。
「Firestoreはそもそもデータ型としてリストやマップを備えているのだから、サブコレクションを利用しなくともそれらを利用すればよいのではないか」つまり、次のようなデータ構造としても同じなのではないか、ということです
users jkfjakdfjaffahi@a name: '鈴木太郎' sex: 'male', articles: [ { id: fjlkafakjfafflakju title: '記事1' body: 'ここに内容が入ります' published: false, createdAt: 1560000000 }確かに、この構造はよく見慣れたJSONの構造であり、必要なデータを一度のクエリで取得できるというメリットもあります。
しかし、以下の点から基本的に階層データはサブコレクションで保持すべきです。ドキュメントのデータサイズには制限がある
前出のとおり、ドキュメントのデータサイズは1MBまでという制限があります。通常の利用には問題ないのですが、上記の例のようにドキュメントのリストやマップにネストした構造は、ユーザーの操作とともに数が増えていくようなデータを保持するには適していません。
クエリ上の観点
FireStoreでのドキュメントに対するクエリは、常にドキュメント全体を返します。
つまり、上記のデータ構造でユーザーデータを取得するとき、必要がないときでも常に記事のデータを取得しなければいけないため、クエリのサイズが大きくなり問題となります。ドキュメントを取得するときに、通常その下の階層にあるサブコレクションは取得されません。サブコレクションは必要なときだけ取得すればよいことになります。
セキュリティルール上の観点
セキュリティルールを設計する際にもネストしたマップやリストを利用している場合問題が生じます。
セキュリティルールでは、for文や一時変数が利用できないため、リストの要素数がドキュメントごとに異なっていたり、ネストしたマップの型が統一されていない場合は安全なスキーマ検証ができなくなります。
さらに、データの秘匿に関しても問題になります。
例えば、ユーザのデータには公開してもよいデータ(名前、プロフィール)と他人には隠しておきたいが、本人は参照したいデータ(メールアドレス、住所)があるはずです。以上のような問題は、サブコレクションによって解決されます。
サブコレクションはJSONツリー型でしかデータを保持できなかったRealTimeDatabaseの弱点を克服した構造ともいえるでしょう。コレクショングループ
さらに、サブコレクションを利用する利点としてコレクショングループを利用することができるという点が上げられます。
コレクショングループは、同一のIDをもつサブコレクションを一つのコレクションとみなして扱うことができる機能です。
通常のクエリでは、Users
のサブコレクションであるArticles
を取得するには、users/{uid}/articles
としてアクセスします。
ユーザーに紐づくすべての記事を取得するには単純ですが、
すべての記事を横断して取得するためにはユーザーごとの記事を取得する必要がありました。しかし、コレクショングループを利用すれば、階層化されたサブコレクションを一度に取得することができます。
コレクショングループクエリを使用するためには、コレクショングループクエリをサポートするインデックスを作成する必要があります。
さらに、ウェブとモバイルSDKの場合は、コレクショングループクエリを許可するルールも作成する必要があります。ドキュメントのデータ型
FireStoreのドキュメントには、以下のデータ型がサポートされています。
- 配列(リスト)
- ブール値
- バイト
- 日時
- 浮動小数点数
- 地理的座標
- 整数
- マップ
- null
- 参照(リファレンス)
- テキスト文字列
FireStoreを使ってみる
Firebaseの概要についてここまで説明してきました。
ここからは、実際にFireStoreを使いながら進めていきます。データベースを有効化する
Firebaseのプロジェクトを作成したら、左のナビゲーションバーから
Database
を選択します。
データベースの作成をクリックして、テストモードを選択しましょう。
テストモードは、誰でもデータベースの読み取りや書き込みが行える状態であるため、決してテストモードのまま本番環境で使用してはいけません。
次に、データベースのロケーションを選択します。
ロケーションは、データを利用するユーザーとサービス近いほどレイテンシが小さくなります。
あなたが日本のユーザーをアプリケーションのターゲットにしているのなら、asia-northeast1(東京)
かasia-northeast1(大阪)
を選択すれば無難でしょう。
データベースの作成が完了したら、次のような画面が表示されます。
データを追加する
それでは、早速データを追加しましょう。
ますはコレクションを開始します。ここでは、users
コレクションを作成します。
コレクションを作成したら、そのまま最初のドキュメントを追加しましょう。
ドキュメントのIDと、ドキュメントのフィールドを追加します。ドキュメントのIDは、なにも入力しなけらばランダムなIDが自動で使用されます。
今回のようにusers
コレクションを作成する場合には、 Firebase Authenticationを利用して作成したユーザーのuid
を指定することが一般的です。
uid
を使用したらドキュメントIDの一意性が確保されますし。ログインしているユーザーの情報を簡単に取得することができます。ドキュメントのフィールドには、キーとタイプ、値を設定します。
キー タイプ 値 name string 鈴木太郎 ドキュメントの追加が完了したら、データが投入されていることが確認できます。
この画面から、さらにコレクションやドキュメントの追加、修正、削除などを行うことができます。
JavaScriptでアプリケーションからFirestoreを利用する
ダッシュボードからFireStoreを利用する方法はわかりましたが、おそらくこれはアプリケーションを利用する上で望んでいる方法ではないでしょう(すべてのアプリケーションの依頼をうけてあなたがデータベースを直接操作するようにしますか?)
Firebase JavaScript SDK
を利用してアプリケーション上から操作できるようにしましょう。開発環境の設定
まずは、アプリケーション上でFireStoreを使えるようにするための設定をします。
Firebaseライブラリの追加
<script>
タグからFirebaseとFirestoreのライブラリをアプリケーションに追加します。<script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-app.js"></script> <script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-firestore.js"></script>または、
npm
からパッケージをインストールします。npm install firebase
npm
を利用した場合には、インストールしたパッケージをimport
しましょう。import firebase from 'firebase/app' import 'firebase/firestore'Firestoreを初期化する
APIキーなどをセットして、Firebaseを初期化します。
Firestoreはfirebase.firestore()
の名前空間から使用できます。if (!firebase.apps.length) { firebase.initializeApp( { apiKey: process.env.VUE_APP_APIKEY, authDomain: process.env.VUE_APP_AUTHDOMAIN, databaseURL: process.env.VUE_APP_DATABASEURL, projectId: process.env.VUE_APP_PROJECTID, storageBucket: process.env.VUE_APP_STORAGEBUCKET, messagingSenderId: process.env.VUE_APP_MESSAGINGSENDERID, appId: process.env.VUE_APP_APPID, measurementId: process.env.VUE_APP_MEASUREMENTID } ) } const db = firebase.firestore()ドキュメントを追加する
これでFirestoreが使えるようになったので、早速基本のCRUD操作からやっていきます。
まずは、ダッシュボード上で行ったようにドキュメントを追加します。IDを指定してドキュメントを追加
IDを指定してドキュメントを追加するには、ドキュメントのリファレンスを作成してから
set()
メソッドを使用します。set()
メソッドは、ドキュメントIDがすでに存在する場合はドキュメントの更新を行い、存在しないドキュメントIDが渡された場合そのドキュメントIDで新規作成をします。cosnt db = firebase.firestore() // ログインしているユーザーの情報を取得します。 const user = firebase.auth().currentUser() // ユーザーコレクションへのリファレンスを作成します。 const userRef = db.collection('user') // ドキュメントIDにはログインユーザーのuidを指定します。 // setの引数にはJavaScriptのオブジェクトの形式でデータを渡します。 userRef.doc(user.uid).set({ name: '鈴木太郎', age: 22, birthday: new Date('1996-11-11') // timestampe型にはDateオブジェクトを渡します。 createdAt: db.FieldValue.serverTimestamp() // サーバーの時間をセットすることもできます。 }) .then(() => // 処理が成功したとき) .catch(e => // エラーが発生したとき)IDを自動で割り当ててドキュメントを追加
IDを自動で割り当ててドキュメントを追加するには、2つの方法があります。
- set()
- add()
なお、2つの方法は完全に等価であり、どちらか好みの方法を利用することができます。
set()を利用する
1つ目の方法は、IDを指定して追加する方法と同じく、
set()
を利用します。
単純に、doc()
に何も渡さなければ、自動的にIDが割り当てられます。cosnt db = firebase.firestore() const userRef = db.collection('user') // doc()の引数にはなにも渡しません userRef.doc().set({ name: '鈴木太郎', age: 22, birthday: new Date('1996-11-11') createdAt: db.FieldValue.serverTimestamp() }) .then(() => // 処理が成功したとき) .catch(e => // エラーが発生したとき)add()を利用する
add()
を利用しても同じようにドキュメントを作成できます。cosnt db = firebase.firestore() const userRef = db.collection('user') // doc()の引数にはなにも渡しません userRef.add({ name: '鈴木太郎', age: 22, birthday: new Date('1996-11-11') createdAt: db.FieldValue.serverTimestamp() }) .then(() => // 処理が成功したとき) .catch(e => // エラーが発生したとき)なお、ドキュメント作成時に存在しないコレクションが指定された場合には、そのコレクションも同時に作成します。
ドキュメントを更新する
ドキュメントを更新するには、以下の2つの方法があります。
- update()
- set()
この2つの方法は細部が異なるので見ていきましょう。
update()を利用する
ドキュメント全体を上書きせずに一部のフィールドを更新するには、
update()
メソッドを利用します。cosnt db = firebase.firestore() const userRef = db.collection('user') // ログインしているユーザーの情報を取得します。 const user = firebase.auth().currentUser() // ageフィールドのみを更新します userRef.doc(user.uid).updaete({ age: 24, }) .then(() => // 処理が成功したとき) .catch(e) = // エラーが発生したとき)set()を利用する
set()
メソッドは前述の通り、既に存在するドキュメントIDを指定した場合ドキュメントを更新します。
しかし、set()
メソッドのデフォルトの動作に注意してください。set()
のデフォルトの動作は引数で与えられた値でドキュメントを置き換えるため、もともと持っていたフィールドはすべて失われ新しいオブジェクトの情報だけ残ります。つまり、
update()
を利用するのと同じ感覚で下記のように指定した場合異なる動作をするおのでは注意が必要です。cosnt db = firebase.firestore() const userRef = db.collection('user') // ログインしているユーザーの情報を取得します。 const user = firebase.auth().currentUser() // ageフィールドのみを更新しようとしましたが、もともと持っていたname、birthday、createdAtは失われてしまいます userRef.doc(user.uid).set({ age: 24, }) .then(() => // 処理が成功したとき) .catch(e) = // エラーが発生したとき)// 予期した結果 { name: '鈴木太郎', age: 24, birthDay: timestampオブジェクト, createdAt: timestampオブジェクト } // 実際の結果 { age: 24 }デフォルトの動作では、変更したくないフィールドも明示的に渡す必要があり、少々不便です。
そこで、一部のフィールドだけを更新したいときは、SetOptions
を第2引数に渡し、merge
パラメータにtrue
を指定します。cosnt db = firebase.firestore() const userRef = db.collection('user') // ログインしているユーザーの情報を取得します。 const user = firebase.auth().currentUser() // ageフィールドのみを更新しようとしましたが、もともと持っていたname、birthday、createdAtは失われてしまいます userRef.doc(user.uid).set({ age: 24, }, { merge: true }) .then(() => // 処理が成功したとき) .catch(e) = // エラーが発生したとき)ドキュメントを削除する
ドキュメントの削除には、
delete()
メソッドを利用します。cosnt db = firebase.firestore() const userRef = db.collection('user') // ログインしているユーザーの情報を取得します。 const user = firebase.auth().currentUser() userRef.doc(user.uid).delete() .then(() => // 処理が成功したとき) .catch(e) = // エラーが発生したとき)単一のドキュメントを取得する
単一のドキュメントを取得するには、
get()
メソッドを利用します。cosnt db = firebase.firestore() const userRef = db.collection('user') // ログインしているユーザーの情報を取得します。 const user = firebase.auth().currentUser() const result = [] userRef.doc(user.uid).get() .then(doc => { if (doc.exists) { result.push({ id: doc.id, ...doc.date() // doc.data()からデータのオブジェクトを取得できます。 }) } else { console.log('結果は空です') } }) .catch(e => // エラーが発生したとき )
get()
が成功したら、doc.exists()
でドキュメントが空でないかチェックします。
ドキュメントが存在したのならば、doc.id
でIDを、doc.data()
からデータのオブジェクトを取得できます。クエリを発行する
単一のドキュメントに対するCRUD操作を見てきました。
しかし、一般的なアプリケーションでは複数のデータを条件によって取得する欲求があるはずです。Firestore
がどのようばクエリを発行できるか見ていきましょう。単純なクエリ
次の例は、すべての記事を返します。
cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef.get() .then(querySnapshot => { if (querySnapshot.empty) { // querySnapshot.emptyがtrueの場合コレクションにデータが存在しません。 console.log('結果は空です') } else { // querySnapshotをループしてデータを取り出します。 querySnapshot.forEach(doc => { // 単一のドキュメントの操作と同じです。 result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )コレクションの参照に
get()
メソッドを利用して、すべてのコレクションを取得することができます。
また、一般的なNoSQL系データベースと異なりFirestore
のクエリ結果はすべて強い整合性をもつことが特徴です。サーバーからドキュメントを取得する場合は
常に最新のデータにアクセスすることが保証されています。フィルタを利用する
Firestore
では、SQLデータベースのようにwhere()
メソッドを利用することでクエリをフィルタリングすることができます。
where()
メソッドは、3つの引数を受け取り、フィルタリングするフィールド、比較演算、値の順に受けれ入れます。
比較演算子には、以下の8つが利用できます。
=
<
<=
>
>=
in
array-contains
array-contains-any
=(等価演算子)
次の例は、ログイン中のユーザーの記事を取得します。
cosnt db = firebase.firestore() // ログインしているユーザーの情報を取得します。 const user = firebase.auth().currentUser() // ユーザードキュメントへの参照を取得 const userRef = db.collection('user').doc(user.uid) // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef // auhtorフィールドは参照型です .where('auhtor', '==', userRef) .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )クエリに
.where('auhtor', '==', userRef)
が追加されています。これが基本的なwhere()
メソッドの使用方法です。< <= > >=(比較演算子)
比較演算子も同じように利用できます。
次の例は、2020年4月以降の記事を取得します。cosnt db = firebase.firestore() // 起点となる日付を作成 // firestoreの日付型はDateオブジェクトで比較できます。 const date = new Date('2020-04') // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef // createdAtフィールドは日付型です .where('createdAt', '>=', date) .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )inクエリ
in
クエリはフィールドがいくつかの値のいずれかに等しいドキュメントを取得します。
in
クエリは、Firestore
で単純なORクエリを実行するのに適した方法です。次の例は、記事のタイトルが「Denoとはなにか - 実際につかってみる」「FireBase①」「JavaScript ES2015」の記事を取得します。
cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef // 配列で値を渡します .where('title', 'in', ['Denoとはなにか - 実際につかってみる', 'FireBase①', 'JavaScript ES2015']) .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )なお、inクエリに渡せる値は10個までという制約があります。
array-contains(配列メンバーシップ)
array_contains
は配列型のフィードに対して使用します。
フィールドの配列に値が含まれていた場合、そのドキュメントを返します。
次の例は、JavaScript
というタグが使用されている記事を取得します。cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef .where('tags', 'array-contains', 'JavaScript') .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )クエリ対象の値が配列内に複数存在する場合でも、ドキュメントは結果に 1 回だけ含まれます。
array-contains-any(配列メンバーシップ)
array-contains-any
は、配列型に対するin
クエリです。cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef .where('tags', 'array-contains-any', ['JavaScript', 'PHP', 'Firebase']) .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )
in
クエリと同様、渡せる値は10までの制約があります。複合クエリ
1回のクエリの中で、複数の
where()
メソッドを呼び出して作成することができます。複合クエリはAND
条件として扱われます。等価演算子=に対する複合クエリ
等価演算子
==
に対する複合クエリには制限がなく、複数回フィルタをかけることができます。cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef .where('auhtor', '==', userRef) .where('createdAt', '==', new Date()) .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )比較演算子に対する複合クエリ
比較演算子に対して複合クエリを使用する場合、1つのフィールドに対するクエリは有効です。
cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef .where('createdAt', '>=', new Date('2019-04')) .where('createdAt', '<', new Date('2020-04')) .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )しかし、複数のフィールドに対して同時に比較演算子を使用することはできません。次のようなクエリはエラーになります。
cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef // 複数のフィールドに対する比較演算子はエラー! .where('createdAt', '>=', new Date('2019-04')) .where('rating', '<', 5) .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )等価演算子と比較演算子、配列メンバーシップを同時に利用する
等価演算子と比較演算子、配列メンバーシップを同時に利用するクエリでは、**複合インデックスを作成する必要があります。
例えば、複合インデックスを作成していない状態で次のようなクエリを発行しようとしてみます。cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef // 等価演算子と比較演算子を同時に利用する .where('createdAt', '>=', new Date('2019-04')) .where('published', '==', true) .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )次のようなエラーが発生してしまいました。
このクエリにはインデックスが必要ですという旨のエラーです。
メッセージに示されたURLをクリックすると、コンソールへ移動して自動的に複合インデックスを作成してくれます。inクエリ、配列メンバーシップ
in
、array-contains
、array-contains-any
は、複合クエリの中で一度だけ使用することができます。クエリのソート
orderBy()
メソッドを使用すると、クエリ結果を並び替えることができます。1回のクエリで複数のフィールドに対してソートをすることができます。
次の例では、作成日の降順、評価の昇順で並び替えます。cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef .order('createdAt', 'desc') // ソート順を指定しなかった場合、昇順になります。 .order('rating') .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )なお、
orderBy()
メソッドは、指定したフィールドの有無によるフィルタも行います。 指定したフィールドがないドキュメントは結果セットには含まれません。
orderBy()
メソッドはwhere()
メソッドと組み合わせて使用することができますが、比較演算子を利用する場合には最初の並べ替えは同じフィールドである必要があります。
次のクエリはエラーになります。cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef .where('createdAt', '>=', new Date('2019-04')) // 比較演算子と異なるフィールドでソートしようとするとエラー .order('rating') .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )さらに、等価演算子を利用して異なるフィールドでソートする際には複合インデックスを作成する必要があります。
cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef .where('published', '==', true) // 複合インデックスの作成が必要 .order('createdAt', 'desc') .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )データの取得数の制限
limit()
メソッドを利用すると、データを取得した数だけ取得します。
次の例では、最新の記事上位10件に限って取得をします。cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] articleRef .order('createdAt', 'desc') // 10件だけ取得 .limit(10) .get() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )ページネーション
Firestore
のクエリを用いてページネーションを行ってみましょう。
limit
句は先程紹介しましたが、offset
句はサポートしておりません。その代わりには、startAfter()
を利用してクエリの開始点を指定することでページネーションを実現します。
startAfter()
には、パラメータドキュメントを渡すことができます。つまり、前回実施したクエリの最後のドキュメントを指定すれば、次のページを取得することができます。cosnt db = firebase.firestore() // 記事一覧への参照を作成 const articleRef = db.collection('articles') const result = [] const limit = 10 // 最後のドキュメントを保持しておきます。 let lastDoc // すべてのドキュメントを取得したかの判定に使用します。 let isFinish = false articleRef .order('createdAt', 'desc') .limit(limit) .startAfter(lastDoc) .get() .then(querySnapshot => { if (querySnapshot.empty) { // 取得したコレクションが空だったらすべてのドキュメントを取得したと判定 isFinish = true } else { if (querySnapshot.size < limit) { // 取得したコレクションの数がlimitよりも少なければこれ以上データはない isFinish = true } // 最後のドキュメントを取得 lastdoc = querySnapshot.docs[querySnapshot.docs.length - 1] querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )2ページ以降も、最初のページと同じ条件のクエリを発行する必要があります。
また、明確にページ数を指定するタイプのページネーションは推奨されていません。(3ページ目を取得しようとしても、2ページ目の終わりがわからない)
無限スクロールによるページネーションの実装が推奨されています。リアルタイムリスナー
Firestore
の大きな特徴の一つとして、リアルタイムリスナーがあります。リアルタイムリスナーは、クライアント側でFirestoreの最新の状態を監視し、変化があった場合には直ちに状態を同期することができます。
リアルタイムリスナーを利用するにはget()
メソッドの代わりにonSnapshot()
メソッドを利用します。cosnt db = firebase.firestore() const articleRef = db.collection('articles') const result = [] articleRef .getonSnapshot() .then(querySnapshot => { if (querySnapshot.empty) { console.log('結果は空です') } else { querySnapshot.forEach(doc => { result.push({ id: doc.id, ...doc.data() }) }) } }) .catch(e => // エラーが発生したとき )リアルタイムリスナーは、例えばチャットのような機能も簡単に実装することができます。
また、ドキュメントがどのような変更がされたか確認することもできます。
cosnt db = firebase.firestore() const articleRef = db.collection('articles') const result = [] articleRef .getonSnapshot() .then(querySnapshot => { snapshot.docChanges().forEach(change { if (change.type === "added") { console.log('追加されたドキュメント', change.doc.data()); } if (change.type === "modified") { console.log('変更されたドキュメント', change.doc.data()); } if (change.type === "removed") { console.log('削除されたドキュメント', change.doc.data()); } }) }リアルタイムリスナーはユーザー体験を向上させますが、単純なクエリのほうが適している場合もあります。
例えば、ブログなどで記事を見ている最中に(今この瞬間ですね)突然本文の内容が変わったり削除されたりすることを好ましいと思う人は少ないでしょう。また、先程のページネーションと組み合わせたりするときも注意が必要です。ページ送りをしている最中にデータの並び順が変わった場合、再度同じドキュメント取得してしまったりなどページ付がおかしくなったりすることがあります。
さらに、データが頻繁に更新されるような場合、データが次々と追加されたり入れ替わるさまを眺めるのは楽しいかもしれませんが、バッテリーや通信量の面でユーザーからは不評を得るかもしれません。
- 投稿日:2020-05-24T17:56:03+09:00
Javascript -イベント フォームに内容を送る- 自分用ノート
読み取りたいフォームの内容を読み取る方法
取得した要素<form>.読みたいりたいフォームのname属性 .value 処理内容 }①定数を定義する
const search =②入力内容を読み取るため
要素を取得(getElementByIdメソッド)const search = document.getElementById('form')③★結構重要。フォームにname属性指定をする
<input type="text" name="word">④Javasctiptのプログラムが読み取る。word(ネーム属性)指定。
const search = document.getElementById('form').word⑤valueプロパティに入れて保存させる
const search = document.getElementById('form').word.value基本動作をキャンセルさせる方法
イベントオブジェクトをファンクションで受け取る
document.getElementById('form') = funcion(event)渡されたのはイベント"オブジェクト"
→プロパティやメソッドを持っているタグの基本動作をキャンセルする働きを持つ.
functionの{〜に追加}event.preventDefalt();
- 投稿日:2020-05-24T17:36:15+09:00
[Django]テンプレートタグから直接JavaScriptの変数に値を渡す方法
Background
TemplateView
のcontext_data
からhtmlにそのまま表示させたい時、下記の通りでタグをセットすると表示することができます。views.pyfrom django.views.generic import TemplateView class SampleTemplateView(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['areas'] = ["japan","tokyo","osaka","nagoya","sapporo","sendai","fukuoka"] return contextindex.html... <ul> {% for item in areas %} <li> {{ item }} </li> {% endif %} </ul> ...
- japan
- tokyo
- osaka
- nagoya
- sapporo
- sendai
- fukuoka
と表示。
しかし、Javascriptのライブラリを使って描画させたい時に直接値を渡したい場合があります。
ここでは渡す方法を紹介します。Failure
そのまま渡すと、、、
<script type="text/javascript"> var areas = "{{ areas }}"; console.log(areas); </script>['japan', 'tokyo', 'osaka', 'nagoya', 'sapporo', 'sendai', 'fukuoka']となってシングルクオテーション、"<"、">"などが自動で変換されます。
文字列型なので正規表現で置換した後にJSON.parse
でdict化でいいかなと思ったのですがうまくできず。で、調べた結果二つの方法を見つけました。
Method 1
泥臭いですが、各要素を呼び出してjavascriptコードに書き込む方法です。
var areas = [ {% for area in areas %} {% with index=forloop.counter0 %} {% if 0 < index%},{% endif %} {% endwith %} "{{area.name}}" {% endfor %} ]; console.log(areas);Method 2
もう一つは、
autoescape
で⾃動エスケープ制御をoffにする方法です。{% autoescape off %} var areas = {{ areas }}; {% endautoescape %} console.log(areas);こっちの方がラクです。
Reference
- 投稿日:2020-05-24T17:20:41+09:00
Safariの文字入力でハマった
シングルクォーテーションだけ入力チェックが正しく動かない
inputタグへの入力文字の制御を行なっていて、PCブラウザやAndroidでは上手くいくのに、iOSだけシングルクォーテーションがうまくいかずハマりました。
keydownイベントでは問題ない
keydownイベントのevent.keyを確認しましたが文字コード0x27「'」で問題なく処理されます。
inputイベントに問題あり
event.target.valueをログ出ししていて、文字コードが0x27「'」でないことに気付きました。
入力の一文字目は\u2018「‘」、二文字目以降は\u2019「’」に勝手に変換されるようです。下記のコードで変換かけてあげて回避できました。
event.target.value = event.target.value.replace(/[\u2018\u2019]/g, '\'');
- 投稿日:2020-05-24T17:00:54+09:00
Gridsomeを触ってみたメモ
JAMstackでVue.js以上Nuxt.js未満なCMS?のイメージのGridsomeを触ってみたのでメモ。
v14で試そうと思ったらうまくいかなかったのでv12にして試したら上手く行きました。(後述)
チュートリアルをなぞってみる
コマンドラインツールのインストール
$ npm i -g @gridsome/cliここが既に重い印象。(ネガ
ブログプロジェクトを作成
$ gridsome create n0bisuke-appここでsharpのビルドエラー
$ gridsome create n0bisuke-app内部でsharpという画像のリサイズとかをしてくれるモジュールを使ってるみたいで、このモジュールがNode.js v14だとビルドが通らなかったです。
省略 info sharp Using cached /Users/n0bisuke/.npm/_libvips/libvips-8.8.1-darwin-x64.tar.gz prebuild-install WARN install No prebuilt binaries found (target=14.2.0 runtime=node arch=x64 libc= platform=darwin) gyp info it worked if it ends with ok gyp info using node-gyp@5.1.0 gyp info using node@14.2.0 | darwin | x64 省略取り急ぎNode.js v12を利用したらビルドが通りました。
ローカル起動
プロジェクト内で以下のコマンドで起動します。
$ gridsome developDONE Compiled successfully in 4292ms 2:10:48 Site running at: - Local: http://localhost:8080/ - Network: http://192.168.2.100:8080/ Explore GraphQL data at: http://localhost:8080/___explore
http://localhost:8080
で起動しました。ビルド
以下のコマンドでビルドができます。 distフォルダが出来上がるのでこれをそのままどこかに載せれば良いみたいです。
$ gridsome buildGridsome v0.7.14 Initializing plugins... Load sources - 0s Create GraphQL schema - 0.03s Create pages and templates - 0.05s Generate temporary code - 0.04s Bootstrap finish - 2.22s Compile assets - 8.7s Execute GraphQL (3 queries) - 0s Write out page data (3 files) - 0s Render HTML (3 files) - 0.27s Process files (0 files) - 0s Process images (9 images) - 1s Done in 12.38svercelでデプロイ
公式ドキュメントにもありますが、
プロジェクトのルートでvercelコマンドを実行するだけでした。カンタン。
$ vercel対話式で聞かれますが全てエンターで大丈夫でした。↓ログです。
Set up and deploy “~/Documents/ds/3_prototypes/n0bisuke-app”? [Y/n] y ? Which scope do you want to deploy to? n0bisuke ? Link to existing project? [y/N] n ? What’s your project’s name? n0bisuke-app ? In which directory is your code located? ./ > Upload [====================] 99% 0.0sAuto-detected project settings (Gridsome): - Build Command: `npm run build` or `gridsome build` - Output Directory: dist - Development Command: gridsome develop -p $PORT ? Want to override the settings? [y/N] n ? Linked to n0bisuke/n0bisuke-app (created .vercel and added it to .gitignore) ? Inspect: https://vercel.com/n0bisuke/n0bisuke-app/fx37uus5b [3s] ✅ Production: https://n0bisuke-app.now.sh [copied to clipboard] [41s] ? Deployed to production. Run `now --prod` to overwrite later (https://zeit.ink/2F). ? To change the domain or build command, go to https://zeit.co/n0bisuke/n0bisuke-app/settingsdistフォルダに移動してからかなぁとか思ってましたがルートの位置で大丈夫です。vercel側がdistフォルダを見てなのかpackage.jsonを見てなのか判断してくれます。
- 投稿日:2020-05-24T15:46:45+09:00
ハンズフリーで考えを書き出す試み(GAS)
やったこと
PCに向かってしゃべった内容を認識しブラウザに書き出します。
エンター
という言葉を認識すると改行し、また要点
という言葉を認識すると■要点
の下に書き込み位置を移動します。背景
考えをまとめるとき、思いつくことをPCで書き出して(タイピングして)してまとめることが多いので、ハンズフリーでできないものかとGASで作ってみました。
茶碗洗いしてるときや、アイロンかけてるときに人と話してる感覚で考えを書き出せたらなと。結果
実用的なレベルには至りませんでした。
- タイピングに比べ入力に時間がかかる
- 誤変換が多い
といった理由で、タイピングに比べ非常にストレスでした。
ただ、
- 音声の認識速度を上げる(前後の文脈をあまり考慮させず、認識した言葉をすぐに確定させる)
- Cloud Speech-to-Textとか使ってもう少し変換精度を上げる
などができれば、ある程度使えるかもしれません。
環境
- 実行環境
- chrome (PCで試しましたが多分スマホでも使えます)
- プログラム
- GAS
- javascript
- Web Speech API
コード
手順は以下です
- googleスプレッドシートなどからスクリプトを作成する
- コード.gsを編集し、index.htmlを新規作成する
- 下記コードをコピペ
- 公開>アプリケーションとして導入>更新
- 表示されたURLにアクセス
- startボタンを押す
特定の言葉を認識した場合、以下の処理をします。
エンター
・・・改行
ブレスト
・・・■ブレスト の下に書き込み
要点
・・・■要点 の下に書き込み
まとめ
・・・■まとめ の下に書き込みコード.gsvar spreadsheet = SpreadsheetApp.getActive(); //シートを取得 var sheet_rec = spreadsheet.getSheetByName("rec"); var last_row = 1; /** include(html名)でhtmlファイルを読み込む */ function include(filename) { return HtmlService.createHtmlOutputFromFile(filename).getContent(); } /** index.htmlを開く */ function doGet() { var template = "index"; return HtmlService.createTemplateFromFile(template) .evaluate() .setSandboxMode(HtmlService.SandboxMode.IFRAME); }index.html<button id="start-btn">start</button> <button id="stop-btn">stop</button> <div id="write-sheet"> <p>■ブレスト</p> <div id="brest"></div> <p>■要点</p> <div id="point"></div> <p>■まとめ</p> <div id="summary"></div> </div> <script> const startBtn = document.querySelector('#start-btn'); const stopBtn = document.querySelector('#stop-btn'); const writeDiv = document.querySelector('#write-sheet'); const brestDiv = document.querySelector('#brest'); const pointDiv = document.querySelector('#point'); const summaryDiv = document.querySelector('#summary'); SpeechRecognition = webkitSpeechRecognition || SpeechRecognition; let recognition = new SpeechRecognition(); recognition.lang = 'ja-JP'; recognition.interimResults = true; recognition.continuous = true; let finalTranscript = ''; // 確定した(黒の)認識結果 let writePosition = brestDiv; recognition.onresult = (event) => { let interimTranscript = ''; // 暫定(灰色)の認識結果 for (let i = event.resultIndex; i < event.results.length; i++) { let transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { finalTranscript += transcript; } else { interimTranscript = transcript; } } finalTranscript = finalTranscript.replace('エンター', '<br>'); // TODO:1回の書き込みで2箇所への移動はできない問題解決 if (finalTranscript.indexOf('ブレスト') != -1) { // 移動前の書き込み writePosition.innerHTML = finalTranscript.substr(0, (finalTranscript.indexOf('ブレスト'))); // 位置フラグ変更 writePosition = brestDiv; // 移動後の書き込み finalTranscript = writePosition.innerHTML + finalTranscript.substr((finalTranscript.lastIndexOf('ブレスト'))); finalTranscript = finalTranscript.replace('ブレスト', ''); } else if (finalTranscript.indexOf('要点') != -1) { writePosition.innerHTML = finalTranscript.substr(0, (finalTranscript.indexOf('要点')) ); writePosition = pointDiv; finalTranscript = writePosition.innerHTML + finalTranscript.substr((finalTranscript.lastIndexOf('要点'))); finalTranscript = finalTranscript.replace('要点', ''); } else if (finalTranscript.indexOf('まとめ') != -1) { // 移動前の書き込み writePosition.innerHTML = finalTranscript.substr(0, (finalTranscript.indexOf('まとめ'))); // 位置フラグ変更 writePosition = summaryDiv; // 移動後の書き込み finalTranscript = writePosition.innerHTML + finalTranscript.substr((finalTranscript.lastIndexOf('まとめ'))); finalTranscript = finalTranscript.replace('まとめ', ''); } writePosition.innerHTML = finalTranscript + '<i style="color:#ddd;">' + interimTranscript + '</i>'; } startBtn.onclick = () => { recognition.start(); } stopBtn.onclick = () => { recognition.stop(); alert("読み取り終わり"); } </script>以上です。
何か気づいた点あればコメントください。
メモ
このプログラムだと、ページを更新すると記入内容がリセットされてしまうので、スプレッドシートに結果を記入できると便利かもしれません。
下記のようなコードで行けると思ったのですが、htmlからgasの関数を呼び出せずエラーとなりました。
暇なときに解決しようと思います...。コード.gsvar spreadsheet = SpreadsheetApp.getActive(); //シートを取得 var sheet_rec = spreadsheet.getSheetByName("rec"); var last_row = 1; function writeHTML(HTMLcontent) { last_row = sheet_rec.getLastRow(); // 空白でない最終行の位置を取得 sheet_rec.getRange(last_row + 1, 2).setValue(HTMLcontent); }index.htmlstopBtn.onclick = () => { recognition.stop(); google.script.run.withSuccessHandler(function () { alert("読み取り終わり"); }).writeHTML(writeDiv.innerHTML); }
- 投稿日:2020-05-24T15:27:27+09:00
JavaScriptでのイベントについて
イベントとは
結論、HTML要素に対して処理要求することです。
例えば、
「ユーザーがブラウザ上のボタンをクリックした」
これはクリックしたというのがイベントに当たります。イベント駆動
JSでは「イベント」が発生したコードが実行される仕組みを持っています。
このイベントを取得するためにはノードに対して処理を書く必要があります。イベントリスナ
ではどのようにして処理を書くのか。
addEventListenerメソッドを使います(ノードオブジェクト).addEventListener("イベント名","関数");これでノードオブジェクトにイベントが起きた時、関数が実行する仕組みです。
実際に具体例でみていきましょう。
このようなウェブページがあるとして、イベントを起こしてみましょう。let btn = document.querySelector("a#btn-square"); function hello() { console.log("Hello world"); } btn.addEventListener("click", hello);本来はこれでHello Worldが出力されるはずですが、
以下のようなエラーが出ることもあります。
これはscriptのタグがどこに書いてあるかに関わってきます。
基本的にscriptを読み込む記述はHTMLのheadタグ内に書き込みます。
ここでもプログラミングの原理原則を思い出してみてください。
上から順番に読み込みます。
その際にボタンのコードが読み込まれる前に
イベントを読み込むためのコードが読み込まれてしまうので
エラーが出てしまうと言った現象です。なので以下のようなメソッドを使いましょう。
①window.onload = function() { 処理 }; ②window.addEventListener('load', function() { 処理 });2通りのパターンがあります。今回は②の方法を使います。
これはページの読み込みが終わったら実行するようになります。
なので先ほどの記述を以下のように訂正します。function HelloWithButton() { let btn = document.querySelector("a#btn-square"); function hello() { console.log("Hello world"); } btn.addEventListener("click", hello); } window.addEventListener("load", HelloWithButton);そうすると、以下の結果になります。
では、手順をまとめます。まとめ
①DOMツリーからノードを取得
②JSでやりたい処理内容を書く
③イベントを発火でHTMLを動かす
- 投稿日:2020-05-24T15:08:09+09:00
DockerでMySQLを起動して、Intelij IDEAで接続する方法
DockerでMySQLを起動して、Intelij IDEAにdockerプラグインを導入して接続する方法を紹介します。画像多めです。
作業環境
OS:Windows 10
エディション:HOME
バージョン:2004事前準備
①Windowsを最新版にアップデートする
下記のサイトを参考に、Windows 10 HOMEの場合は必ず最新版までアップデートしておきます。
https://www.atmarkit.co.jp/ait/articles/1701/07/news037.html
最新版になるまで、何回もやる可能性があります。下のような画面になれば、OKです。
②IntelliJ IDEAのインストールする
以下のサイトを参考にIntelliJ IDEAの Community Edition をインストールし、日本語化します。
https://sukkiri.jp/technologies/ides/intellij-idea/intellij-idea-win.html
※Editionが2つあるので、注意してください。
③Dockerをインストールする
以下のサイトを参考に、Dockerをインストールします。
https://techracho.bpsinc.jp/ebi/2020_03_27/90477Windows 10 HOMEの場合は、インストール方法が特殊なので、下記のサイトを参考にインストールしてください。
https://tech.guitarrapc.com/entry/2020/04/21/034236④Dockerの設定
Dockerのインストールが終わったら、起動して、上の歯車のアイコンを押して、設定を開きます。
「General」の中にある「Expose daemon on …」をオンにし、下にある「Apply & Restart」を押します。
※Dockerの画面が出ない場合は、ここからアクセスします。
サンプルソースをダウンロードする
今回は下記のサイトから、サンプルコードをダウンロードします。
https://github.com/miyabayt/spring-boot-doma2-sample
下の画像にように、「Clone or download」という緑のボタンを押すと、下のような画面が出てくるので、「Download ZIP」を押してダウンロードします。
ZIPファイルをダウンロードして、デスクトップなどに解凍してから、Cドライブ直下に移動してください。
私の環境下では、Cドライブ直下に解凍すると、下記のように解凍されました。元のフォルダの構成がとは異なるので、エラーの原因になります。上記の画像と同じ構成になるように解凍してください。
IntelliJ IDEAにプラグインを導入する
事前準備が完了したら、IntelliJ IDEAに4つプラグインを導入します。
①Lombok pluginをインストールする
「構成」→「プラグイン」を押します。
下記のような、プラグインを追加する画面に変わります。
赤枠で囲ったところに「Lombok」と入力すると プラグインが表示されるので、インストールボタンを押して、インストールします。
②Eclipse Code Formatterをインストールする
次に「Eclipse Code Formatter」と入力すると プラグインが表示されるので、インストールボタンを押して、インストールします。
③Dockerをインストールする
次に「Docker」と入力すると プラグインが表示されるので、インストールボタンを押して、インストールします。
④Python Community Editionをインストールする
次に「Python Community Edition」と入力すると プラグインが表示されるので、インストールボタンを押して、インストールします。
※このプラグインはIntelliJ IDEAからおすすめされて入れたので、不要の可能性もあります。
プラグインのインストールが終わったら、OKを押すと前の画面に戻ります。導入したプラグインを設定する
次にプラグインの設定をしていきます。
「構成」→「設定」を押し、設定の画面を開きます。
①Lombok pluginの設定
左側の「ビルド、実行、デプロイ」を押し、「コンパイラー」を開き、その中の「注釈プロセッサー」を押します。
右側の画面が変わるので、「注釈処理を使用可能にする」にチェックを入れ、上の画面のようになればOKです。②Eclipse Code Formatterの設定
左側の「その他の設定」→「Eclipse Code Formatter」を押します。
右側の画面が変わるので、「Use the Eclipse code formatter」にチェックを入れ、「Eclipse Java Formatter config file」の右の「参照」を押すと、ファイル選択の画面が出てきます。
先程解凍した「spring-boot-doma2-sample-master」というフォルダを開き、その中の「eclipse-formatter.xml」を選びます。eclipse-formatter.xmlまでのパスは、Cドライブ直下に「spring-boot-doma2-sample-master」がある場合は、下記の通りになります。
C:\spring-boot-doma2-sample-master\eclipse-formatter.xml③Dockerの設定
左側の「ビルド、実行、デプロイ」を押し、「Docker」を押します。
「TCPソケット」を選び、接続が完了すればOKです。
接続できない場合は、まずはDockerが起動しているかどうか確認してください。以上でプラグインの設定は終了です。
IntelliJ IDEAの設定を変更する
プラグインの設定が完了したら、IntelliJ IDEAの設定を変更していきます。
①bootRunを実行している場合でもビルドされるようにする
Intellij上で「Ctrl+Shift+A」を押すと、小さい画面が出てくるので、「レジストリー」と入力します。
すると下に「レジストリー」と出てくるので、ここを選びます。
「compiler.automake.allow.when.app.running」という項目探して、右側のチェックボックスにチェックを入れます。終わったら、「閉じる」を押します。②コンソール出力の文字化けを防ぐ
Windowsの場合は、コンソール出力が文字化けするため、C:¥Program Files¥JetBrains¥IntelliJ Idea xx.x.x¥binの中にある「idea64.exe.vmoptions」というファイルを開きます。
一番下の行に「-Dfile.encoding=UTF-8」を追記して保存します。
③Java11を設定する
「構成」→「プロジェクト構造」を選びます。
設定が開くので、「プロジェクト設定」→「プロジェクト」を開き、「プロジェクトSDK」と「プロジェクト言語レベル」をそれぞれ、「11」を選びます。
もし、「プロジェクトSDK」で「11java version “11.0.7”」といった項目が出ない場合は、以下のページより「Java SE 11 (LTS)」の「JDK Download」からダウンロードして、インストールしてください。
https://www.oracle.com/java/technologies/javase-downloads.html※詳しいインストール手順が必要な場合はこちらを参考にしてみてください。
https://sukkiri.jp/technologies/processors/jdk/jdk-win_install.htmlDockerでMySQLを起動する
Dockerを起動します。Dockerの画面が出ない場合は、ここからアクセスします。
白いクジラのアイコンの上で右クリックをして、「Dashboard」を選びます。
Dockerの画面が出てきたら、赤枠で囲った▶部分を押します。
MySQLが出てくるので、赤枠で囲ったボタンを押すと、サーバーの起動が始まります。
下の画面が出てくればOKです。
初回起動の場合
初回起動で、上記の画面が出てこない場合は、以下の手順でMySQLを起動してください。
①docker-compose.ymlが存在するフォルダを確認する
サンプルコードの中には、DockerでMySQLを起動するための設定ファイル「docker-compose.yml」が含まれています。
Cドライブ直下に「spring-boot-doma2-sample-master」がある場合は、「C:\spring-boot-doma2-sample-master\docker」にあります。②Windows Power Shell を管理者権限で起動する
Dockerにコンテナなどを追加するので、タスクバーのwindowsマークの上で右クリックして、Windows Power Shell を管理者権限で起動します。
③Windows Power ShellからDockerのMySQLを起動する
急にWindows Power Shellが出てきて、難しそうですが、入力するのは以下の2行だけです。
cd C:\spring-boot-doma2-sample-master\docker docker-compose up先程の「docker-compose.yml」というファイルが存在するフォルダまで移動します。Cドライブ直下に「spring-boot-doma2-sample-master」がある場合は、以下のようになります。
cd C:\spring-boot-doma2-sample-master\docker“docker-compose”というコマンドが使えるかどうかを確かめるために、以下のように入力します。
docker-compose --version以下のようにバージョンが表示されればOKです。
docker-compose version 1.25.5, build 8a1c60f6次に以下のように入力して、MySQLを起動します
docker-compose upエラーが出てうまく行かない場合は、docker-compose.ymlが存在するフォルダまで移動してから、上記のコードを実行しているかどうか、確認してください。
このコマンドが終了すると、Dockerの画面が下記のように変わります。
簡単にDockerでMySQLを起動することができますね。Dockerで起動したMySQLに、Intelij IDEAを接続する
Intelij IDEAを起動して、「オープンまたはインポート」を選びます。
Cドライブ直下に解凍した「spring-boot-doma2-sample-master」を選び、OKを押します。
下のような画面が出てくるので、「サービス」を選びます。
画面が変わると「Docker」が出てくるので、選ぶと接続が始まります。
接続が完了すると、以下のような画面になります。
以上で、Dockerで起動したMySQLに、Intelij IDEAで接続することができました。サンプルコードを動かす
ここからは、サンプルコードを動かしていきます。
サンプルコードを動かす前に、プラグインの設定とJavaの設定を確認してください。
確認が終了したら、Intelij IDEAの「ターミナル」を開きます。
①バージョン確認
ターミナルが起動したら、以下のように入力します。
cd C:\spring-boot-doma2-sample-master次に、以下のようにコードを入力します。
gradle -v以下のように、バージョンが表示されます。
------------------------------------------------------------ Gradle 6.3 ------------------------------------------------------------ Build time: 2020-03-24 19:52:07 UTC Revision: bacd40b727b0130eeac8855ae3f9fd9a0b207c60 Kotlin: 1.3.70 Groovy: 2.5.10 Ant: Apache Ant(TM) version 1.10.7 compiled on September 1 2019 JVM: 11.0.7 (Oracle Corporation 11.0.7+8-LTS) OS: Windows 10 10.0 amd64②サンプルコードを動かす
次にサンプルコードを動かします。以下のように入力します。
gradlew composeUpプログラムが終わったら、以下のように入力します。
gradlew :sample-web-admin:bootRunしばらく待つと、95%で止まりますが、そのままでOKです。
これでアプリが起動したので、http://localhost:18081/adminにアクセスします。
上のような画面が出てきたら、成功です。お疲れさまでした。この記事は先輩エンジニアからアドバイスを受け、個人ブログから移植しました。おかしいところなどありましたら、ご指摘いただけると幸いです。
- 投稿日:2020-05-24T14:49:32+09:00
Playwrightで簡単なクロスブラウザテストを書いてみた
2020 年 5 月の頭頃、Playwright が 1.0 に到達した際に、作成チームによって書かれた記事がホットエントリーになっていました。
https://medium.com/@arjunattam/fast-and-reliable-cross-browser-testing-with-playwright-155c0e8a821fタイミング良くクロスブラウザテストを書く機会があったので、簡単なテストを書いてみました。
※ ブラウザ毎に挙動の異なる API のテストだと良かったのですが、こちらで実施するのはボタンをクリックして innerText の変更をテストするのみです。ソースはこちらにあります。
https://github.com/nnashiki/playwright_samplePlaywrigh とは
Chromium、Firefox、Safari を簡単にクロスブラウザテストするための、Node library とのことです。
Playwright is a Node library to automate Chromium, Firefox and WebKit with a single API. Playwright is built to enable cross-browser web automation that is ever-green, capable, reliable and fast.
https://playwright.dev/
https://github.com/microsoft/playwright冒頭に挙げた medium の記事では、Chromium、WebKit、FireFox、WebDriver などに協力を得た(巨人の肩に乗ってるんだぜ!)とあります。
アーリアダプターはこんな感じに充実しています。テスト対象
"ボタン押下前"と書かれているボタンを押すと"ボタン押下前"に変わる簡単なものです。
以下コマンドでサービスを立ち上げます。
docker run --rm --name sample -p 8090:80 -v $PWD/src/app:/usr/share/nginx/html -d nginx
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <title>ボタンテスト</title> <link rel="icon" href="favicon.ico" /> <!- bootstrap4 -> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" /> <!- bootstrap4 -> </head> <body> <div class="container"> <button id="test_button1" type="button" class="btn btn-primary"> ボタン押下前 </button> </div> <script type="text/javascript" src="./index.js?0.0"></script> <!- bootstrap4 -> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous" ></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous" ></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous" ></script> <!- bootstrap4 -> </body> </html>index.jsconst button1 = document.getElementById("test_button1"); button1.addEventListener("click", (e) => { document.getElementById(e.target.id).innerText = "ボタン押下後"; });テストコード
利用コード
test runner は Jest を使用しています。
各ブラウザで自動操縦でボタンをクリックして、表記の変化を確認するテストです。
ブラウザ単位でテスト結果を確認したいので分割しました。src/test/button_action.test.jsconst playwright = require("playwright"); // #test_button1 のボタンを押下して、ボタンの表記を取得する const croll_click = async (browserType) => { const browser = await playwright[browserType].launch(); const context = await browser.newContext(); const page = await context.newPage(); await page.goto("http://localhost:8090"); await page.click("#test_button1"); const click_after_text = await page.evaluate(() => { return document.getElementById("test_button1").innerText; }); await browser.close(); return click_after_text; }; test("click chromium", async () => { let result = await croll_click("chromium"); expect(result).toMatch(/ボタン押下後/); }); test("click firefox", async () => { let result = await croll_click("firefox"); expect(result).toMatch(/ボタン押下後/); }); test("click webkit", async () => { let result = await croll_click("webkit"); expect(result).toMatch(/ボタン押下後/); });page の中での javascript(webAPI)の実行は以下を参考にしました。
https://playwright.dev/#version=v1.0.2&path=docs%2Fverification.md&q=evaluating-javascriptJest の set up、tear down でブラウザの呼び出しと終了をするには以下が参考になります。
https://playwright.dev/#version=v1.0.2&path=docs%2Ftest-runners.md&q=jest--jasmine環境構築
Playwright、 Jest を install します。
npm install --save-dev playwright npm install --save-dev jest実行してみる
jest src/test
を実行します。
ブラウザ毎のテストを実行することができました。> jest src/test PASS src/test/button_action.test.js (7.1 s) ✓ click chromium (1327 ms) ✓ click firefox (4186 ms) ✓ click webkit (1073 ms) Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total Snapshots: 0 total Time: 7.594 s Ran all test suites matching /src\/test/i.tips
そのまま実行すると、Jest の default time out が引っかかることがありました。
FAIL src/test/button_action.test.js (8.725 s) ✓ click chromium (2076 ms) ✕ click firefox (5002 ms) ✓ click webkit (1018 ms) ● click firefox : Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error: 22 | }); 23 | > 24 | test("click firefox", async () => { | ^ 25 | let result = await croll_click("firefox"); 26 | expect(result).toMatch(/ボタン押下後/); 27 | }); at new Spec (node_modules/jest-jasmine2/build/jasmine/Spec.js:116:22) at Object.<anonymous> (src/test/button_action.test.js:24:1) Test Suites: 1 failed, 1 total Tests: 1 failed, 2 passed, 3 total Snapshots: 0 total Time: 9.605 s, estimated 12 s Ran all test suites.そこで、timeout を伸ばして実行しました。
jest src/test --testTimeout=10000
5000mm を超えても実行できています。$ jest src/test --testTimeout=10000 PASS src/test/button_action.test.js (11.592 s) ✓ click chromium (3831 ms) ✓ click firefox (5042 ms) ✓ click webkit (1948 ms) Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total Snapshots: 0 total Time: 12.957 s Ran all test suites matching /src\/test/i.まとめ
puppeteer は使った事が無いので、比較はできませんが利用の開始はとても簡単でした。
Playwright は普及しそうなので、今後も追ってみたいと思います。
- 投稿日:2020-05-24T14:46:27+09:00
TypeScriptで学ぶデザインパターン〜Flyweight編〜
対象読者
- デザインパターンを学習あるいは復習したい方
- TypeScriptが既に読めるあるいは気合いで読める方
- いずれかのオブジェクト指向言語を知っている方は気合いで読めると思います
- UMLが既に読めるあるいは気合いで読める方
環境
- OS: macOS Mojave
- Node.js: v12.7.0
- npm: 6.14.3
- TypeScript: Version 3.8.3
本シリーズ記事一覧(随時更新)
- TypeScriptで学ぶデザインパターン〜Iterator編〜
- TypeScriptで学ぶデザインパターン〜Adapter編〜
- TypeScriptで学ぶデザインパターン〜Template Method編〜
- TypeScriptで学ぶデザインパターン〜Factory Method編〜
- TypeScriptで学ぶデザインパターン〜Singleton編〜
- TypeScriptで学ぶデザインパターン〜Prototype編〜
- TypeScriptで学ぶデザインパターン〜Builder編〜
- TypeScriptで学ぶデザインパターン〜Abstract Factory編〜
- TypeScriptで学ぶデザインパターン〜Bridge編〜
- TypeScriptで学ぶデザインパターン〜Strategy編〜
- TypeScriptで学ぶデザインパターン〜Composite編〜
- TypeScriptで学ぶデザインパターン〜Decorator編〜
- TypeScriptで学ぶデザインパターン〜Visitor編〜
- TypeScriptで学ぶデザインパターン〜Chain of Responsibility編〜
- TypeScriptで学ぶデザインパターン〜Facade編〜
- TypeScriptで学ぶデザインパターン〜Mediator編〜
- TypeScriptで学ぶデザインパターン〜Observer編〜
- TypeScriptで学ぶデザインパターン〜Memento編〜
- TypeScriptで学ぶデザインパターン〜State編〜
- TypeScriptで学ぶデザインパターン〜Flyweight編〜
Flyweightパターンとは
インスタンスを共有するためのパターンです。
サンプルコード
Flyweightパターンで作られたクラス群がどんなものになるのか確認していきましょう。
今回は、題材として"数値を出力する簡単な機能"を想定します。GitHubにも公開しています。
modules/Number.ts
数値を表現するクラスです。
Number.tsexport default class Number { private value: number; constructor(value: number) { this.value = value; } getValue(): number { return this.value; } }どのメソッドも特に解説の必要はないので割愛します。
modules/NumberFactory.ts
数値インスタンスを作成するクラスです。
NumberFactory.tsimport Number from "./Number"; export default class NumberFactory { private static singleton: NumberFactory = new NumberFactory; private numbers: { [key: number]: Number; } = {}; private constructor() {} static getInstance(): NumberFactory { return NumberFactory.singleton; } getNumber(value: number): Number { if (!(value in this.numbers)) { console.log(value + ': インスタンス生成'); this.numbers[value] = new Number(value); } return this.numbers[value]; } }
getInstance
にある通り、Singletonパターンになっています。
getNumber
ではNumberインスタンスを生成して返却します。ただ数値が重複していた場合は新たにインスタンスを作成せず、インスタンスを保持しているプロパティであるnumbersから返却します。modules/Main.ts
Flyweightパターンで作成されたクラス群を実際に使用している処理です。
Main.tsimport NumberFactory from "./modules/NumberFactory"; const numberFactory: NumberFactory = NumberFactory.getInstance(); const firstNumber: number = numberFactory.getNumber(1).getValue(); const secondNumber: number = numberFactory.getNumber(2).getValue(); const thirdNumber: number = numberFactory.getNumber(3).getValue(); const fourthNumber: number = numberFactory.getNumber(2).getValue(); console.log(firstNumber); console.log(secondNumber); console.log(thirdNumber); console.log(fourthNumber);
firstNumber
とSecondNumber
とThirdNumber
は新たにインスタンスが作られていますが、fourthNumber
だけは新たにインスタンスは作られません。secondNumber
と同様だからです。クラス図
ここまでFlyweightパターンで作られたクラス群を1つずつ確認してきました。次にクラス図を示します。Flyweightパターンの全体像を整理するのにお役立てください。
- Flyweight: サンプルコードでは
State
インターフェースが対応- FlyweightFactory: サンプルコードでは
DayState
クラスNightState
クラスが対応- Client: サンプルコードでは
ClockContext
が対応※LucidChartを使用して作成
解説
最後に、このデザインパターンの存在意義を考えます。
インスタンスを作成するとメモリを使用します。メモリは無限に使えるわけではないので、複数作成する必要のないインスタンスを共有して使用できることがメリットです。インスタンスを共有するので様々なところで呼ばれている場合は修正時の影響範囲が大きいという問題もあるので注意が必要です。
補足
サンプルコードの実行方法はこちらと同様です。
参考
あとがたり
そろそろ書くことなくなってきたわ。とりあえずあともう少しGoFデザインパターン完走頑張るぞよ。