- 投稿日:2020-01-13T23:36:39+09:00
初心者による フォームの扱い
概要
クライアントjavascriptにおいて、フォームはエンドユーザからの入力を受け取る代表的な手段である。Webページに必ずあるといってよいものであるため、十分に学習しておきたい。
入力ボックス/選択ボックス
入力ボックス、選択ボックスの入力を受け取る方法はvalueプロパティにアクセスするだけである。
sample.html<form> <label for="name">名前:</label> <input id="name" name="name" type="text" size="30" /> <input id="btn" type="button" value="送信" /> </form> <div id="result"> </div>sampl.jsdocument.addEventListener('DOMContentLoaded', function(){ document.getElementById('btn').addEventListener('click', function() { let name = document.getElementById('name') //入力された内容をvalueで取得 console.log(name.value) }, false) }, false)入力ボックス類とselectはvalueプロパティによってすべて同じ要領で取り出すことができる。また、これらの要素は設定するときもvalueプロパティに代入することですぐできる。
チェックボックス
チェックボックスの値にアクセスするのは、入力ボックスに比べて複雑です。
sample.html<form> <div> <label><input type="checkbox" name="food" value="ラーメン" />ラーメン</label> <label><input type="checkbox" name="food" value="餃子" />餃子</label> <label><input type="checkbox" name="food" value="焼肉" />焼肉</label> <input id="btn" type="button" value="送信" /> </div> </form>sample.jsdocument.addEventListener('DOMContentLoaded', function() { document.getElementById('btn').addEvenetListener('click', function() { let resulr = [] let foods = document.getElementsByName('food') for(let i = 0, len = foods.length; i < len; i++){ let food = foods.item(i) //チェックされているかを調べるには、checkedプロパティを使う if(food.checked){ result.push(food.value) } } //配列を文字列にして出力 window.alert(result.toString()) }, false) }, false)チェックボックスを使う場合は、
checked : チェックされているかどうか value : value="" で指定されている値であることに注意する。
また、チェックボックスは複数とは限らず、オンオフの単一の場合も考えられる。その場合は特別に次のようにする。sample.jslet onoff = getElementById('onoff') if(onoff.checked){ console.log(onoff.value) }else{ console.log('チェックされていません') }チェックボックスの値をjavascriptで設定する
sample.html<form> <div> <label><input type="checkbox" name="food" value="ラーメン" />ラーメン</label> <label><input type="checkbox" name="food" value="餃子" />餃子</label> <label><input type="checkbox" name="food" value="焼肉" />焼肉</label> <input id="btn" type="button" value="送信" /> </div> </form>sample.jsdocument.addEventListener('DOMContentLoaded', function() { //ここでのvalue引数は配列 let setCheckValue = function(name, value) { let elems = document.getElementsByName(name) for(let i = 0, len = elems.length; i < len; i++){ let elem = elems.item(i) if(value.indexOf(elem.value) > -1){ elem.checked = true } } } setCheckValue('food', ['餃子', '焼肉']) }, false)ここでは、Array.indexOf(value)メソッドを使っている。これは、配列Arrayの中にvalueと等しいものがあるのかをチェックします。存在しなければ、-1を返すため-1よりも大きいと条件づければよい。
ラジオボタン
ラジオボタンはチェックボタンとほとんど同じ要領であるが、より汎用性を持つように汎用的なコードを別に関数として定義して、それを使うようにする。
sample.html<form> <div> <label><input type="checkbox" name="food" value="ラーメン" />ラーメン</label> <label><input type="checkbox" name="food" value="餃子" />餃子</label> <label><input type="checkbox" name="food" value="焼肉" />焼肉</label> <input id="btn" type="button" value="送信" /> </div> </form>sample.jsdocument.addEvenetListener('DOMContentLoaded', function() { //自作関数を作る let getRadioValue = function(name) { let result = '' let elems = document.getElementsByName(name) for(let i = 0, len = elems.length; i < len; i++){ let elem = elems.item(i) if(elem.cheked){ result = elem.value //チェックされているのは一つしかない break } } return result } //自作関数を実行 document.getElementById('btn').addEventListener('click', function() { window.alert(getRadioValue(food)) }, false) }, false)関数を自作することで、汎用性を持たせることができます。
ラジオボタンの値をjavascriptで設定する
sample.js<form> <div> <label><input type="checkbox" name="food" value="ラーメン" />ラーメン</label> <label><input type="checkbox" name="food" value="餃子" />餃子</label> <label><input type="checkbox" name="food" value="焼肉" />焼肉</label> <input id="btn" type="button" value="送信" /> </div> </form>sample.jsdocumnt.addEventListener('DOMContentLoaded', function() { let setRadioValue = function(name, value){ let elems = gtElementsByName(name) for(let i = 0, len = elems.length; i < len; i++){ let elem = elems.item(i) if(elem.value = value){ elem.check = true //見つけ終わったら抜ける break } } } setRadioValue('food', '餃子') }, false)リストボックス
sample.html<form> <div> <label for="food">好きな食べ物は?:</label> <select id="food" multiple> <option value="ラーメン">ラーメン</option> <option value="餃子">餃子</option> <option value="焼肉">焼肉</option> </select> <input id="btn" type="button" value="送信" /> </div> </form>sample.jsdocument.addEventListener('DOMContentLoaded', function() { let getListValue = function(name) { let result = [] //option要素はoptionsでアクセスする! let opts = document.getElementsById(name).options for(let i = 0, len = opts.length; i < len; i++){ let opt = opts.item(i) //checkedではなくselectedを使うことに注意 if(opt.selected){ result.push(opt.value) } } return result } document.getElementById('btn').addEvenetListener('click', function() { window.alert(getListValue('food')) }, false) }, false)ここでは、option要素の集まりをoptionsプロパティによって取り出していることに注意する。チェックボックスでは、checkedプロパティを使っていたが、リストボックスでは、selectedプロパティを使う。
リストボックスの値をjavascriptで設定する
内容は上記のものと何ら変わらないため例だけ載せておく。
sample.html<form> <div> <label for="food">好きな食べ物は?:</label> <select id="food" multiple> <option value="ラーメン">ラーメン</option> <option value="餃子">餃子</option> <option value="焼肉">焼肉</option> </select> <input id="btn" type="button" value="送信" /> </div> </form>sample.jsdocument.addEvenetListener('DOMContentLoaded', function() { let setListValue = function(name, value) { let opts = document.getElementsByName(name) for(let i = 0, len = opts.length; i < len; i++){ let opt = opts.item(i) if(value.indexOf(opt.value) > -1){ opt.selected = true } } } setListValue('food', ['餃子', '焼肉']) }, false)参考資料
山田祥寛様 「javascript本格入門」
- 投稿日:2020-01-13T23:31:50+09:00
頼むからGraphQLのクエリを動的に組み立てるのはやめてくれ
主にapolloの話。
https://github.com/Shopify/graphql-js-client のようなクエリビルダを使ってるんだったら別。
ここでいう「動的に」というのは、下記のように通常の文字列結合で引数とかselection setを突っ込むようなコードを指している。
apolloClient.query(gql` query { procucts(${variables}) { id } } `);当たり前なんだけど、
variables
に"first: 10) { __typename } users(first: 10"
みたいな文字列を突っ込んだら、想定とは全く異なるクエリになって何が起こるかわかったもんじゃない。変数はGraphQLのvariablesに格納するようにしろ。
apolloClient.query(gql` query MyQuery($first: Int!) { procucts(first: $first) { id } } `, { variables: { first } }));要は クエリは静的に決定された状態にしておけ というのが言いたいだけなんだけど、実行結果が想定外になる、というだけじゃなく、他にも弊害が色々ある。
例えば冒頭のコード、
gql
というTagged Templateを使ってて、apolloでコード書いたらよく出てくるんだけど、こいつは中身の文字列をparseしてGraphQLのASTに変換する役割を持っている。gql
は一度parseしたASTは元のクエリ文字列をキーにしたObjectに格納してcacheとして利用するように実装されている。折角cacheを持っているのにそこにhitしなかったらメモリの無駄、というのもあるし、このcacheはただObjectにガンガン値突っ込んでるだけで、LRUとかそういうことは一切していないので、SSRの文脈でこのコードをNode.jsのサーバーで動かしたらメモリリークを引き起こす。クエリの文字列をメモリ上に持ち続ける、というのは persisted queryの文脈などでも出てくる。この場合はクエリ文字列のhash値をキーに、parseしたASTやクエリ文字列を値にした辞書を中間層に保持することになるが、もしクエリが動的にコロコロ変わるようになっていたら、こういった仕組みだってやはり真当に動作しない。
大概のケースだと、codegenとかその手の静的な解析ツールを書けたときにこういう行儀の悪いコードは見つけられると思っていたけど、
apollo client:codegen
コマンドだと特にエラーも警告も吐かずにそれっぽい型ファイルを生成してしまうのも質が悪い。
- 投稿日:2020-01-13T23:20:01+09:00
JavaScript で配列に特定の要素が含まれているか確認する
Array.prototype.indexOf()
メソッドを使用して、配列に特定の要素が含まれているか確認する。例
配列に特定の要素が含まれている場合は
true
、そうでない場合は、false
を返す。console.log(['foo', 'bar', 'baz'].indexOf('foo') !== -1); // true console.log(['foo', 'bar', 'baz'].indexOf('fo') !== -1); // falseArray.prototype.indexOf()
indexOf()
メソッドは、引数に指定した内容と同じ配列要素を探し、最初の要素の添字を返す。
存在しない場合は、-1
を返す。例
console.log(['foo', 'bar', 'baz'].indexOf('foo')); // 0 console.log(['foo', 'bar', 'baz'].indexOf('bar')); // 1 console.log(['foo', 'bar', 'baz'].indexOf('ba')); // -1
- 投稿日:2020-01-13T22:33:01+09:00
TypeScriptでユーザー定義のエラーを実装する
例外周りにあまり知見がなかったのでJavaScriptの標準のエラーから、TypeScriptでユーザー定義の例外を実装する方法までを調べました。
JavaScriptの標準のエラーオブジェクトについて
JavaScriptの標準のエラーオブジェクトは以下の表の通り7つあります。
エラーオブジェクト 説明 Error ランタイムエラーが発生した時に投げられます。ユーザー定義の例外の基底オブジェクトとして使用することもできます。 EvalError グローバルな eval() 関数に関連するエラーを示します。この例外はもう JavaScript から投げられませんが、EvalError オブジェクトは互換性のために残っています。 RangeError 値が配列内に存在しない、または値が許容範囲にない場合のエラーを表します。 ReferenceError 存在しない変数が参照された場合のエラーを表します。 SyntaxError 構文的に不正なコードを解釈しようとした場合のエラーを表します。 TypeError 値が期待される型でない場合のエラーを表します。 URIError グローバル URI 処理関数が誤った使い方をされたことを示すエラーです。 こんなに定義されていたのですね。恥ずかしながらRangeErrorしか知りませんでした
TypeScriptでユーザー定義の例外を実装
先ほどの表ではAPIの通信エラーがあった場合、適切なエラーが見つからないのでErrorオブジェクトを使うことになります。
Errorオブジェクトは、ユーザー定義の例外の基底オブジェクトとして使用することもできます。
とのことなので、より適切な
HttpRequestError
というエラーを作ってみましょう。
まずErrorオブジェクトを継承しApplicationError
をユーザー定義の基底オブジェクトを作成します。ApplicationError.tsexport default class ApplicationError extends Error { constructor(public message: string) { super(message); this.name = "ApplicationError"; } } // 最初はErrorインタフェースを実装していたのですが、 // 後ほど使うSentry.captureExceptionでErrorを継承する必要があった // export default class ApplicationError implements Error {
ApplicationError
を継承したHttpRequestError
を作成。HttpRequestError.tsimport ApplicationError from "./ApplicationError"; export default class HttpRequestError extends ApplicationError { constructor(public message: string, public status: number) { super(message); this.name = "HttpRequestError"; this.message = `${status}: ${message}` } } throw new HttpRequestError("管理者に連絡してください", 500)標準エラーとユーザー定義エラーにより異なるアクションを実行
標準エラーとユーザー定義のエラーに基づいて、エラーハンドラー内で異なるアクションを実行します。(Vueのエラーハンドラーについてはコチラ)
errorHandler.tsVue.config.errorHandler = (error, vm, info) => { // ApplicationErrorの場合はユーザーにアラートを表示し if (error instanceof ApplicationError) { alert(error.message); if (error.stack) { Sentry.captureException(error); } } // ビルトインのエラーについてはユーザーに表示せず、Sentryなどで開発者に通知する if (error instanceof Error) { if (error.stack) { Sentry.captureException(error); } } };さいごに
JavaScriptの例外のベストプラクティスを探して見つけることができませんでした。
自分で調べた結果このような形に落ち着いたのでしばらくはこの形を使用していく予定です。間違いやアドバイスがあれば教えていただけると嬉しいです!
- 投稿日:2020-01-13T19:45:22+09:00
JavaScriptでスムーススクロールを実装する
こんなやつ
動作デモ → https://jsfiddle.net/zr36gcpb/コード
HTML<div class="scroll"> <a class="scroll__button" href=".contents__1">Scroll</a> <a class="scroll__button" href=".contents__2">Scroll</a> <a class="scroll__button" href=".contents__3">Scroll</a> <a class="scroll__button" href=".contents__4">Scroll</a> </div> <div class="contents"> <div class="contents__1">Contents1</div> <div class="contents__2">Contents2</div> <div class="contents__3">Contents3</div> <div class="contents__4">Contents4</div> </div>JavaScriptconst smoothScroll = e => { const target = document.querySelector(e.target.getAttribute('href')); const position = target.getBoundingClientRect().top + window.scrollY; e.preventDefault(); window.scrollTo({ top: position, behavior: 'smooth' }); //window.scrollToの引数のtopは「ドキュメントの左上を基準にした目標の要素のY座標のピクセル値」で、 //behaviorにはsmoothを指定する事で挙動がスムーススクロールになります。 } const buttons = document.querySelectorAll('.scroll__button'); buttons.forEach(button => { button.addEventListener('click', smoothScroll); })上記の関数
smoothScroll
を任意のaタグ
に走らせることで、擬似的にhref
で指定した要素にスムーススクロールするボタンになります。やっている事は単純で、
aタグ
に指定されているhref
の値を取得して、変数position
にその指定された要素の「ビューポートの左上からのY座標のピクセル数」と「現在スクロールしているY座標のピクセル数」を足したもの(=対象の要素の絶対位置)を格納しています。
そのposition
の値を使用して、window.scrollTo
で目標の位置までスクロールしています。
そして、もちろんaタグ
のデフォルトの動作をキャンセルするpreventDefault
も指定します。もし
aタグ
以外で使用したい場合は、href
の代わりにdata属性
を指定してそれをgetAttribute
すれば良いと思います。
出来ました。(*゚▽゚ノノ゙☆パチパチパチ※注意点
一部のブラウザでパラメーター部分が非対応なため、普通のスクロールになってしまう事がある。
ブラウザの実装状況(MDN)
- 投稿日:2020-01-13T19:39:49+09:00
初心者による DOM 属性値の取得/設定
概要
getElementById()等で要素ノードにアクセスできたら、次は、その要素ノードに何らかの処理をしたい。
特定の属性の取得
多くの属性は「要素ノードの同名のプロパティ」としてアクセスできるからです。たとえば、
sample.js//取得 let url = link.href //設定 link.href = 'https://google.com'ただし、classに関してはclassNameになることが注意点です。この「属性とプロパティは一致しない場合がある」ことを意識したくないのであれば、次の方法がある。
sample.jselem.getAttribute(name) elem.setAttribute(name, value) //elem : 要素オブジェクト name : 属性名 value : 属性値これを用いて書き直すと、
sample.jslet url = link.getAttribute('href') link.setAttribute('href', 'https://yahoo.com')getAttributeとsetAttributeを使うメリットとしては、
1. HTMLとjavascriptとで名前の相違を意識する必要はない。
2. (文字列として指定できるため)取得/設定する属性名を、スクリプトから動的に変更できる。
が挙げられる。不特定の属性の所得
一つ一つ属性を取得するのではなく、ある特定の要素ノードに属する属性値をすべて取得したい場合はAttributesをつかう。例えば、
sample.html<img id="logo" src="https://sample.to/image/1.png" height="57" width="10" alt="image" />sample.jsdocument.addEventListener('DOMContentLoaded', function(){ let logo = document.getElementById('logo') let attrs = logo.attributes for(let i = 0, let len = attrs.length: i < len; i++){ let attr = attrs.item(i) //nameとvalueプロパティでアクセス。 console.log(attr.name + ',' + attr.value) } }, false) //出力 id:logo src:https://sample.to/image/1.png height:57 width:10 alt:image取り出した属性ノードの名前や値にアクセスするには、name/valueプロパティを使う。この例で使われている attrs はNamedNodeMapオブジェクトと呼ばれ、この属性値のマップを操作することも可能。次のように、
samplejs//title属性を作成 let title = document.createAttribute('tilte') title.value = 'ロゴ画像' //マップに追加と削除 attrs.setNamedItem(title) attrs.removeNamedItem(title)テキストの取得/設定
要素配下のテキストを取得/設定するには、innerHTML / textContentというプロパティを利用する。
sample.html<div id="result_text"> <p style="color: red;">設定されていません!</p> </div> <div id="result_html"> <p style="color: red;">設定されていません!</p> </div>sample.jsdocument.addEventListener('DOMContentLoaded', function() { documentGetElementById('result_text').textContent = '<a href="https://google.com">Google</a>' documentGetElementById('result_html').innerHTML = '<a href="https://yahoo.com">yahoo</a>' }, false)これで重要なことは、「配下の子要素/テキストを完全に書き換えている」ということです。しかし、決定的に違うところは、「与えられたテキストをHTMLとして認識するかどうか」である。一般的には、textContentプロパティを優先して利用することで、高速かつ安全に運用できる。
また、console.log()ではinnerHTMLプロパティはHTML文字列すべてを返すが、textContentはその中のテキストだけを取り出して出力することに注意する。innerHTMLの注意点
innerHTMLプロパティを利用する場合、ユーザからの入力値をはじめ、外部からの入力値をそのまま渡さないようにしなければならない。
sample.html<form> <label for="name">名前:</label> <input id="name" name="name" type="text" /> <input id="btn" type="button" value="送信" /> </form> <div id="result"></div>sample.jsdocument.addEventListener('DOMContentLoaded', function() { let name = document.getElementById('name') let result = document.getElementById('result') //この文が問題の文! result.innerHTML = 'こんにちは、' + name.value + 'さん!' })result.innerHTMLの文が好ましくないところである。なぜならば、innerNTMLはHTML認識可能な文を第三者が入力可能であるため、製作者が意図しないコードを入力してくる可能性がある。しかもそれを不特定多数の人がブラウザー上で実行することができるのである。このような脆弱性をクロスサイトスクリプティング(XSS)脆弱性という。innerHTMLではなくtextContentにすることが望ましい。
参考資料
山田祥寛様 「javascript本格入門」
- 投稿日:2020-01-13T19:33:42+09:00
便利ページ:QRコードスキャン
久しぶりの、「便利ページ:Javascriptでちょっとした便利な機能を作ってみた」 のシリーズものです。
今回は、QRコードスキャンです。
QRコード生成はすでに実装してあったのですが、スキャンする方はありませんでした。HTML5では、PCに接続されたカメラを扱うことができますので、ブラウザだけでスキャンできます。また、AndroidやiPhoneでのChromeでも動作するので、これでPCだけでなくスマホでも動きます。
いつもの通りGitHubにも上げてあります。
https://github.com/poruruba/utilities参考までに、以下にデモとしてアクセスできるようにしてあります。
https://poruruba.github.io/utilities/QRスキャンのためのライブラリ
以下を使わせていただきました。ありがとうございます。
cozmo/jsQR
https://github.com/cozmo/jsQRHTMLで以下ようにスクリプトをロードしておきます。
<script src="dist/js/jsQR.js"></script>
ソースコード抜粋
肝心のJavascriptのソースコードです。重要部分のみ抜粋しています。
start.jsqrcode_scan: function(){ this.qrcode_video = $('#qrcode_camera')[0]; this.qrcode_canvas = $('#qrcode_canvas')[0]; if( this.qrcode_running ){ this.qrcode_forcestop(); return; } this.qrcode_running = true; this.qrcode_context = this.qrcode_canvas.getContext('2d'); this.qrcode_timer = setTimeout(() =>{ this.qrcode_forcestop(); }, QRCODE_CANCEL_TIMER); return navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" }, audio: false }) .then(stream =>{ this.qrcode_video.srcObject = stream; this.qrcode_draw(); }) .catch(error =>{ alert(error); }); }, qrcode_draw: function(){ this.qrcode_context.drawImage(this.qrcode_video, 0, 0, this.qrcode_canvas.width, this.qrcode_canvas.height); const imageData = this.qrcode_context.getImageData(0, 0, this.qrcode_canvas.width, this.qrcode_canvas.height); const code = jsQR(imageData.data, this.qrcode_canvas.width, this.qrcode_canvas.height); if( code ){ this.qrcode_scaned_data = code.data; console.log(code); this.qrcode_forcestop(); this.qrcode_context.strokeStyle = "blue"; this.qrcode_context.lineWidth = 3; var pos = code.location; this.qrcode_context.beginPath(); this.qrcode_context.moveTo(pos.topLeftCorner.x, pos.topLeftCorner.y); this.qrcode_context.lineTo(pos.topRightCorner.x, pos.topRightCorner.y); this.qrcode_context.lineTo(pos.bottomRightCorner.x, pos.bottomRightCorner.y); this.qrcode_context.lineTo(pos.bottomLeftCorner.x, pos.bottomLeftCorner.y); this.qrcode_context.lineTo(pos.topLeftCorner.x, pos.topLeftCorner.y); this.qrcode_context.stroke(); this.qrcode_btn = 'QRスキャン開始'; }else{ if( this.qrcode_running ) requestAnimationFrame(this.qrcode_draw); } }, qrcode_forcestop: function(){ if( !this.qrcode_running ) return; this.qrcode_running = false; if( this.qrcode_timer != null ){ clearTimeout(this.qrcode_timer); this.qrcode_timer = null; } this.qrcode_video.pause(); this.qrcode_btn = 'QRスキャン開始'; },ご参考までに、HTMLの方も。
index.html<label>scaned data</label> <div class="input-group"> <span class="input-group-btn"> <button class="btn btn-default clip_btn glyphicon glyphicon-paperclip" data-clipboard-target="#qrcode_scaned_data"></button> </span> <input id="qrcode_scaned_data" type="text" class="form-control" v-model="qrcode_scaned_data" readonly> </div><br> <button class="btn btn-primary" v-on:click="qrcode_scan()">{{qrcode_btn}}</button><br> <div> <img v-show="!qrcode_running && qrcode_scaned_data==''" id="qrcode_start" src="./img/qr_start.png" v-bind:style="qrcode_size"><br> <video v-show="qrcode_running" id="qrcode_camera" v-bind:style="qrcode_size" autoplay></video> <canvas v-show="!qrcode_running && qrcode_scaned_data!=''" id="qrcode_canvas" v-bind:style="qrcode_size"></canvas> </div>解説
・qrcode_scan()
ボタン押下をトリガに、QRコードスキャンを開始します。
まずは、「navigator.mediaDevices.getUserMedia」を呼び出して、ユーザに対してカメラ利用の許可を聞いた後にカメラを起動させます。
起動した後、「this.qrcode_video.srcObject = stream;」でカメラ画像をWebページに表示させ、「qrcode_draw()」の呼び出しで、カメラ画像からQRコードを探します。
setTimeout()がありますが、一定時間QRコードスキャンで見つからなかったときに、QRコードスキャンを停止するためのものです。
ちなみに、ブラウザからカメラを利用するため、HTMLはHTTPSでホスティングしている必要があります。・qrcode_draw()
カメラ画像からQRコードをスキャンします。
いったん、別のcanvasのcontextにコピーしたのち、QRコードスキャンのライブラリを呼び出します。
QRコードがあった場合には、QRコードの部分を青色の四角形で囲ってスキャン終了です。
もしQRコードがなかった場合には、「requestAnimationFrame」を呼び出して次のカメラ画像を待ちます。以上
- 投稿日:2020-01-13T19:33:42+09:00
便利ページ:JavascriptでQRコードスキャン
久しぶりの、「便利ページ:Javascriptでちょっとした便利な機能を作ってみた」 のシリーズものです。
今回は、QRコードスキャンです。
QRコード生成はすでに実装してあったのですが、スキャンする方はありませんでした。HTML5では、PCに接続されたカメラを扱うことができますので、ブラウザだけでスキャンできます。また、AndroidやiPhoneでのChromeでも動作するので、これでPCだけでなくスマホでも動きます。
いつもの通りGitHubにも上げてあります。
https://github.com/poruruba/utilities参考までに、以下にデモとしてアクセスできるようにしてあります。
https://poruruba.github.io/utilities/QRコードスキャンのためのライブラリ
以下を使わせていただきました。ありがとうございます。
cozmo/jsQR
https://github.com/cozmo/jsQRHTMLで以下ようにスクリプトをロードしておきます。
<script src="dist/js/jsQR.js"></script>
ソースコード抜粋
肝心のJavascriptのソースコードです。重要部分のみ抜粋しています。
start.jsqrcode_scan: function(){ this.qrcode_video = $('#qrcode_camera')[0]; this.qrcode_canvas = $('#qrcode_canvas')[0]; if( this.qrcode_running ){ this.qrcode_forcestop(); return; } this.qrcode_running = true; this.qrcode_btn = 'QRスキャン停止'; this.qrcode_scaned_data = ""; this.qrcode_context = this.qrcode_canvas.getContext('2d'); this.qrcode_timer = setTimeout(() =>{ this.qrcode_forcestop(); }, QRCODE_CANCEL_TIMER); return navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" }, audio: false }) .then(stream =>{ this.qrcode_video.srcObject = stream; this.qrcode_draw(); }) .catch(error =>{ alert(error); }); }, qrcode_draw: function(){ this.qrcode_context.drawImage(this.qrcode_video, 0, 0, this.qrcode_canvas.width, this.qrcode_canvas.height); const imageData = this.qrcode_context.getImageData(0, 0, this.qrcode_canvas.width, this.qrcode_canvas.height); const code = jsQR(imageData.data, this.qrcode_canvas.width, this.qrcode_canvas.height); if( code && code.data != "" ){ this.qrcode_scaned_data = code.data; console.log(code); this.qrcode_forcestop(); this.qrcode_context.strokeStyle = "blue"; this.qrcode_context.lineWidth = 3; var pos = code.location; this.qrcode_context.beginPath(); this.qrcode_context.moveTo(pos.topLeftCorner.x, pos.topLeftCorner.y); this.qrcode_context.lineTo(pos.topRightCorner.x, pos.topRightCorner.y); this.qrcode_context.lineTo(pos.bottomRightCorner.x, pos.bottomRightCorner.y); this.qrcode_context.lineTo(pos.bottomLeftCorner.x, pos.bottomLeftCorner.y); this.qrcode_context.lineTo(pos.topLeftCorner.x, pos.topLeftCorner.y); this.qrcode_context.stroke(); }else{ if( this.qrcode_running ) requestAnimationFrame(this.qrcode_draw); } }, qrcode_forcestop: function(){ if( !this.qrcode_running ) return; this.qrcode_running = false; if( this.qrcode_timer != null ){ clearTimeout(this.qrcode_timer); this.qrcode_timer = null; } this.qrcode_video.pause(); this.qrcode_video.srcObject = null; this.qrcode_btn = 'QRスキャン開始'; },ご参考までに、HTMLの方も。
index.html<label>scaned data</label> <div class="input-group"> <span class="input-group-btn"> <button class="btn btn-default clip_btn glyphicon glyphicon-paperclip" data-clipboard-target="#qrcode_scaned_data"></button> </span> <input id="qrcode_scaned_data" type="text" class="form-control" v-model="qrcode_scaned_data" readonly> </div><br> <button class="btn btn-primary" v-on:click="qrcode_scan()">{{qrcode_btn}}</button><br> <div> <img v-show="!qrcode_running && qrcode_scaned_data==''" id="qrcode_start" src="./img/qr_start.png" v-bind:style="qrcode_size"><br> <video v-show="qrcode_running" id="qrcode_camera" v-bind:style="qrcode_size" autoplay></video> <canvas v-show="!qrcode_running && qrcode_scaned_data!=''" id="qrcode_canvas" v-bind:style="qrcode_size"></canvas> </div>解説
・qrcode_scan()
ボタン押下をトリガに、QRコードスキャンを開始します。
まずは、「navigator.mediaDevices.getUserMedia」を呼び出して、ユーザに対してカメラ利用の許可を聞いた後にカメラを起動させます。
起動した後、「this.qrcode_video.srcObject = stream;」でカメラ画像をWebページに表示させ、「qrcode_draw()」の呼び出しで、カメラ画像からQRコードを探します。
setTimeout()がありますが、一定時間QRコードスキャンで見つからなかったときに、QRコードスキャンを停止するためのものです。
ちなみに、ブラウザからカメラを利用するため、HTMLはHTTPSでホスティングしている必要があります。・qrcode_draw()
カメラ画像からQRコードをスキャンします。
いったん、別のcanvasのcontextにコピーしたのち、QRコードスキャンのライブラリを呼び出します。
QRコードがあった場合には、QRコードの部分を青色の四角形で囲ってスキャン終了です。
もしQRコードがなかった場合には、「requestAnimationFrame」を呼び出して次のカメラ画像を待ちます。以上
- 投稿日:2020-01-13T19:18:21+09:00
Svelteで使えるCSSフレームワークまとめてみた
はじめに
Svelteで使えるCSSフレームワークを調べたのでまとめます。
sveltestrap
HP:https://bestguy.github.io/sveltestrap/
GitHub:https://github.com/bestguy/sveltestrap
Star:181
もとになったフレームワーク:BootStrap
※2020/01/13現在Svelma
HP:https://c0bra.github.io/svelma/
GitHub:https://github.com/c0bra/svelma
Star:200
もとになったフレームワーク:Bulma
※2020/01/13現在Svelte Material UI
HP:https://sveltematerialui.com/
GitHub:https://github.com/hperrin/svelte-material-ui
Star:546
もとになったフレームワーク:Material-ui
※2020/01/13現在おわりに
ざっと調べると紹介した3つが使えそうだなと思いました。
これも使えるよとかありましたらコメントで教えてください!
- 投稿日:2020-01-13T18:41:31+09:00
react にて、画像リンク切れの際、imgタグを非表示にする
通常のhtmlにおいては、onerrorハンドラにてリンク切れのimgタグを非表示にできます。
<img src="original.png" alt="title" onerror="this.style.display='none'"/>reactではこれができないため、以下のように
onError
ハンドラを作成します。<img src={`${process.env.PUBLIC_URL}/logo192_.png`} onError={e => e.target.style.display = 'none'} />サンプル
?以下のサンプルは、
create-react-app
直後のApp.js
を編集したものです。App.jsimport React from 'react'; function App() { return ( <div className="App"> <img border="1" src={`${process.env.PUBLIC_URL}/logo192.png`} /> <img border="1" src={`${process.env.PUBLIC_URL}/logo192_.png`} /> <img border="1" src={`${process.env.PUBLIC_URL}/logo192_.png`} onerror="this.style.display='none'" /> <img border="1" src={`${process.env.PUBLIC_URL}/logo192_.png`} onError={e => e.target.style.display = 'none'} /> </div> ); } export default App;参考
?Hide broken image link in Semantic UI React
?リンク切れの時に代替画像を表示したいときはこちら。
- 投稿日:2020-01-13T18:11:15+09:00
【復習】JavaScriptで複数のclassに反映させる方法
困ったこと
classを指定しているのに一つしか反映されない。。。
理解と対応
classの場合は配列になるので一つ一つ取り出さなければいけない
そんな時に利用するのが
for文for ( 初期値; 繰り返す条件; 増減値 ) { // 繰り返す処理を書く }配列の取り出し方としては
box[1]やbox[2]などで取り出すため
変数を用いてbox[i]と記載しfor文で変数の値を増やしていけば取り出すことが可能実際のコード
<style> .box1 { width: 100px; height: 100px; background: red; } .box2 { width: 100px; height: 100px; background: red; } </style> <body> <div class="box1" id="jj"></div> </br> <div class="box2" id="hh"></div> </br> <div class="box2" id="hh"></div> <script> var box = document.getElementsByClassName('box2'); for (var i = 0 ; i < box.length ; i++ ){ box[i].style.background = 'pink'; } </script>
- 投稿日:2020-01-13T17:55:29+09:00
ejectなしのExpoアプリにStripe決済をWebViewを使って埋め込む(Checkout編)
Payment IntentとElementsを使った記事に引き続き、Stripe CheckoutでStripeによって生成されるページをWebViewで使用してみます。
Checkoutページに遷移させるにはStripe.jsの
redirectToCheckout
メソッドを使用しますが、その際、sku
を指定する場合(Checkout クライアント専用組み込み)と、サーバー側でセッションを作成してからsessionId
を指定する場合と2種類の方法があります。
クライアント専用組み込みの場合はサーバー側の実装が不要ですが、リダイレクト元に関して設定の必要と制限がある*ため、この記事では後者の方法をとります。*URLスキームが
http(s)
に限られるので、WebView上の非ホスティングのHTMLから直接リダイレクトできない。1. サーバーサイドの準備
PaymentIntentの時と同じように、今度はセッションを作成してアプリに返すAPIを作成します。
セッションの内容は適当に入れているので、用途に合わせて適宜変更してください。functions/index.jsconst functions = require('firebase-functions'); const app = require('express')(); const stripe = require('stripe')('sk_test_...'); // 秘密鍵 const cors = require('cors'); const bodyParser = require('body-parser'); app.use(require('body-parser').text()); app.use(cors()); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); const SUCCESS_URL = 'https://.../loading.html#succcess'; // 成功時のコールバックURL const CANCEL_URL = 'https://.../loading.html#canceled'; // キャンセル時のコールバックURL app.post('/createSession', async (req, res) => { const { amount, currency, locale } = req.body; const result = await stripe.checkout.sessions.create({ success_url: SUCCESS_URL, cancel_url: CANCEL_URL, locale, payment_method_types: ['card'], line_items: [ { name: 'test item', description: 'test description', amount, currency, quantity: 1 } ] }); res.json(result); }); exports.api = functions.https.onRequest(app);上記のように、Checkoutの際は
success_url
とcancel_url
としてコールバックURLを指定する必要があります。
最初はこれらのリダイレクト先をどこにもホスティングせずWebViewでなんとかしようとしたのですが思うように制御できなかったため、とりあえずローディング画面のようなページだけはどこかに用意することにしました。成功/キャンセルページを作成
See the Pen dyPKpXg by mildsummer (@mildsummer) on CodePen.
ほとんど見えないページなのでなんでもよいです。
このような感じで何もしないページをどこかにホスティングして、先ほどのAPIでURLを返すようにします。2. アプリ側を作成
完成イメージ
ボタンを押したらモーダル内のWebViewでCheckoutページを表示するような形にします。
WebViewを使ったコンポーネントを作成
StripeCheckout.jsimport React, { Component } from 'react'; import { View, ActivityIndicator } from 'react-native'; import * as PropTypes from 'prop-types'; import { WebView } from 'react-native-webview'; /** * Stripe決済をWebViewを介して行うコンポーネント */ class StripeCheckout extends Component { constructor(props) { super(props); this.state = { ready: false, session: null }; this.init(); } /** * セッションを作成 * @returns {Promise<void>} */ init = async () => { const { amount, currency, locale } = this.props; const response = await fetch('https://[リージョン]-[プロジェクトID].cloudfunctions.net/api/createSession', { method: 'POST', headers: { 'Content-type': 'application/json' }, body: JSON.stringify({ amount, currency, locale }) }); const session = await response.json(); this.setState({ session }); }; /** * WebViewからのデータを処理 * @param event */ onMessage = (event) => { const json = JSON.parse(event.nativeEvent.data); console.log(json); }; /** * ページ読み込み完了時 */ onLoadEnd = (event) => { const { session } = this.state; const { url } = event.nativeEvent; const { onSucceeded, onCanceled } = this.props; if (url === session.success_url) { onSucceeded(); } else if (url === session.cancel_url) { onCanceled(); } else if (url !== 'about:blank') { this.setState({ ready: true }); } }; render() { const { ready, session } = this.state; const { publicKey, style } = this.props; const html = session && ` <!DOCTYPE html> <html> <head> </head> <body> <script src="https://js.stripe.com/v3/"></script> <script> var stripe = Stripe('${publicKey}'); try { stripe .redirectToCheckout({ sessionId: '${session.id}' }) .then(function(result) { postMessage(result.error.message); }).catch(function(e) { postMessage(e.message); }); } catch (e) { postMessage(e.message); } /** * アプリ側にデータを送る * @param data */ function postMessage(data) { window.ReactNativeWebView.postMessage(JSON.stringify(data)); } </script> </body> </html> `; return ( <View style={style}> {!ready && <ActivityIndicator style={{ width: '100%', height: '100%' }}/>} {session && ( <WebView onLoadEnd={this.onLoadEnd} javaScriptEnabled={true} source={{ html }} onMessage={this.onMessage} style={{ width: '100%', height: '100%' }} /> )} </View> ); } } StripeCheckout.propTypes = { publicKey: PropTypes.string.isRequired, amount: PropTypes.number.isRequired, currency: PropTypes.string, locale: PropTypes.string, style: PropTypes.object, onSucceeded: PropTypes.func.isRequired, onCanceled: PropTypes.func.isRequired }; StripeCheckout.defaultProps = { currency: 'JPY', locale: 'ja', style: null }; export default StripeCheckout;ActivityIndicatorなど適宜表示しつつ、コンポーネント初期化時に先ほどのAPIでセッションを作成し、その後WebViewを表示。
WebViewのロードイベントをハンドリングすることでどのページが読み込まれているかどうかを判断して、コールバックURLに遷移した時にprops経由で親要素に通知します。画面を作成
上記のコンポーネントをモーダル内に表示して、支払いの流れを作ります。
モーダルはreact-native-modal
を使用します。App.jsimport React, { Component } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Modal from 'react-native-modal'; import StripeCheckout from './StripeCheckout'; export default class App extends Component { constructor(props) { super(props); this.state = { isCheckout: false, succeeded: false }; } /** * Checkoutモーダルを表示 */ checkout = () => { this.setState({ isCheckout: true }); }; /** * Checkoutをキャンセル */ cancel = () => { this.setState({ isCheckout: false }); }; /** * Checkout成功 */ onSucceeded = () => { this.setState({ isCheckout: false, succeeded: true }); }; render() { const { isCheckout, succeeded } = this.state; return ( <View style={styles.container}> <Text style={styles.title}>注文のテスト</Text> <TouchableOpacity onPress={this.checkout}> <View style={[ styles.button, succeeded && styles.succeededButton ]} > <Text style={[styles.buttonText, succeeded && styles.succeededButtonText]} > {succeeded ? '注文が完了しました' : '注文する'} </Text> </View> </TouchableOpacity> <Modal isVisible={isCheckout} style={styles.modal} onBackButtonPress={this.cancel} onBackdropPress={this.cancel} > <StripeCheckout style={styles.modalInner} amount={1000} publicKey="pk_test_..." // 公開鍵 onSucceeded={this.onSucceeded} onCanceled={this.cancel} /> </Modal> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, height: '100%', width: '100%', backgroundColor: '#ffffff', alignItems: 'center', justifyContent: 'center' }, title: { position: 'relative', fontSize: 24, fontWeight: 'bold', marginBottom: 24 }, modal: { justifyContent: 'flex-end', margin: 0 }, modalInner: { height: '70%', backgroundColor: '#ffffff' }, button: { position: 'relative', width: 240, height: 50, borderRadius: 25, justifyContent: 'center', alignItems: 'center', backgroundColor: 'orange' }, succeededButton: { borderColor: 'orange', borderWidth: 2, backgroundColor: 'transparent' }, buttonText: { fontSize: 18, fontWeight: 'bold', color: '#ffffff' }, succeededButtonText: { color: 'orange' } });これで完成です。ダッシュボードでも決済が確認できました。
- 投稿日:2020-01-13T17:41:22+09:00
?祝日とかぶった予定を自動で削除する(Google Apps Scriptで)
やること: 祝日とかぶった予定を自動で削除する
会議や打ち合わせの予定を繰り返しイベント(予定)で登録していませんか?私もその1人です。
この機能、便利なのですが、イベントが祝日にかぶったとき、いちいち手で消すのが面倒なのですよね…
(例えば、毎週月曜に定例の会議を入れているけど、来週は祝日だから中止だ、とかのケースです)今回は祝日の検知と、その日のイベントを自動キャンセルするというスクリプトを実践してみましょう。
やってみよう
手元で動かしているロジックは、もっといろんなことをやっているので、本質的な部分を切り出して紹介します。
(間違いを見つけられましたら、指摘してもらえると嬉しいです。)言うまでもありませんが、自己責任でお願いします。予定を削除するメソッドが含まれますので、バックアップを取るとか、削除の部分はコメントアウトして動かすなど、各自対策していただければ。
function removeBusinessEventOnHoliday() { // 通知用(ここではSlackを使う) const slackApp = new SlackApp(); // 評価期間(月単位) const from = Moment.moment().startOf("Month").toDate(); const to = Moment.moment().add(1, "Months").startOf("Month").toDate(); // Googleが公開している日本の祝日カレンダー const holidayCalendar = CalendarApp.getCalendarById("ja.japanese#holiday@group.v.calendar.google.com"); // 自分のカレンダー const myCalendar = CalendarApp.getDefaultCalendar(); // 日本の祝日カレンダーと既定のカレンダーの「特別休日」(職場の休みなど)を抽出 [ holidayCalendar.getEvents(from, to), dmyCalendar.getEvents(from, to, { search: "特別休日" }) ].forEach(function (holidays) { holidays.forEach(function (holiday) { const holidayName = holiday.getTitle(); const holidayFrom = holiday.getStartTime(); const holidayTo = holiday.getEndTime(); // 祝休日と重複する「定例」「共有会」を抽出 ["定例", "共有会"].forEach(function (query) { myCalendar.getEvents(holidayFrom, holidayTo, { search: query }).forEach(function (event) { if (event.isRecurringEvent()) { const eventDate = Moment.moment(event.getStartTime()); // 通知 slackApp.sendText( eventDate.format("M/D") + "は[" + holidayName + "]のため、" + (event.isAllDayEvent() ? "" : eventDate.format("H:mm") + "開始の") + "[" + event.getTitle() + "]は取り消されます。" ); // イベントの削除 event.deleteEvent(); } }); }); }); }); }まずは評価期間内の祝日を洗い出そう
私の場合、毎月1日にこのロジックを動かすようにしています。
なので評価期間は当月初~来月初の1ヶ月間です。
日付計算はMoment.jsを使っています。とても便利です。
ライブラリに組み入れて動かしてください。(参考: https://tonari-it.com/gas-moment-js-moment/)// 評価期間(月単位) const from = Moment.moment().startOf("Month").toDate(); const to = Moment.moment().add(1, "Months").startOf("Month").toDate();そして、Googleが公開している日本の祝日カレンダーと、自分のカレンダーから評価期間内の「お休み」を抽出します。
自分のカレンダーにも会社の休み(「特別休日」)が入っているので、2つのカレンダーから抽出しています。
会社が公休などをカレンダーで公開してくれているのなら、そうしたものを使いましょう。<ポイント>
- 日本の祝日: Googleの日本の祝日カレンダー
- 会社の公休: 自分のカレンダーの「特別休日」というイベント// Googleの日本の祝日カレンダー const holidayCalendar = CalendarApp.getCalendarById("ja.japanese#holiday@group.v.calendar.google.com"); // 自分のカレンダー const myCalendar = CalendarApp.getDefaultCalendar(); // 日本の祝日カレンダーと既定のカレンダーの「特別休日」(会社の休みなど)を抽出 [ holidayCalendar.getEvents(from, to), myCalendar.getEvents(from, to, { search: "特別休日" }) ].forEach(function (holidays) { // 後述 }); }祝日に行われているイベントを抽出しよう
祝日が取得できたら、祝日の開始~終了までに行われているイベントを抽出しましょう。
ただし、いくつか前提を設けています。
1. 消す対象は「定例」と「共有会」に限定しました。(お好みで)
2. 消す対象は「繰り返しイベント」に限定しています。
3. 祝日をまたぐようなイベントは想定しない。(冬期休暇とか)// 祝休日と重複する「定例」「共有会」を抽出 ["定例", "共有会"].forEach(function (query) { defaultCalendar.getEvents(holidayFrom, holidayTo, { search: query }).forEach(function (event) { if (event.isRecurringEvent()) { // 通知…ここでは割愛 // イベントの削除 event.deleteEvent(); } }); }); });最後に
最後まで読んでくださってありがとうございます。
今回はちょっと簡単過ぎでしたでしょうか最初、祝日テーブルをスプレッドシートに置こうとしていたのですが、祝日のメンテナンスをしたくないので、Googleカレンダーを使わせてもらいました。
日本は祝日が多い国ですが、特にスクリプトが重いということはなかったです。月に1~2日程度ですからね。それでは。
- 投稿日:2020-01-13T17:22:36+09:00
JavaScript: var/letのスコープとletによる巻き上げ
「入門JavaScriptプログラミング」
https://www.amazon.co.jp/dp/479815864X
この本でJavaScriptの学習中。このタイトルをしておきながら
「ES5以前のJavaScriptを理解している人向けに、ES2015以降の機能について紹介する」
というやや凶悪(?)な一面を持つな本だ。タイトルはその第1章の内容。
もうvarは使わない気がするが、2点知らなかった内容があったので記録に残す。基本:letとvarのscope
- varは関数スコープ ``
if (true) { var foo =
bar`; }console.log(foo); // "bar"
```
- letはブロックスコープ ``
if (true) { let foo =
bar`; }console.log(foo); // fooはブロックの外側に存在しないためエラー
```この辺はただの前提。以下が本題。
varの関数スコープによる厄介な影響
<ul> <li>one</li> <li>two</li> <li>three</li> <li>four</li> <li>five</li> </ul> ... <script> var items = document.querySelectorAll; for (var i = 0; i < 5; i++) { var li = item[i]; li.addEventListener('click', () => { alert(li.textContent + ':' + i); }); } </script>上のコードはhtmlの各リストアイテムをクリックすると、対応したメッセージのアラートが表示される、ということを意図して書かれている。が、実際には期待通りには動作しない。どのリストアイテムをクリックしても、表示されるのは"five: 5"になる。
var i
とvar li
は関数スコープなので、5回のforループの全てで共有されている。
つまりどのリストアイテムをクリックしても、結局は同じメッセージのアラートが表示される。
forの最後のループが終わった際、'i = 4'の時のループが終わった際には、i = 5
, li =<li>five</li>
になっているため、前述の通り必ず"five: 5"が表示される。varの代わりにletを使えば意図した動作になる。
letはブロックスコープであるため、forの各ブロックごとにlet i
,let li
は独立して存在することになるからだ。正直なところもうvarを自分で書くことはないと思うが、既存コードに存在する場合はこの点注意が必要になりそうだ。
letによる変数の巻き上げ
letによる変数による巻き上げとは、「変数が宣言されたスコープの内側では、変数がスコープ全体を消費する」と言うことだ。
これだけだとさっぱり分からないのでコードで説明する。let num = 10; const getNum = () { return num; } console.log( getNum() );このコードはコンソールに10を表示する。getNum()内にnumの宣言が無い為、スコープチェーンの仕組みに則って、外側にある
let num = 10;
が用いられる。let num = 10; const getNum = () { let num = 5 // 変更箇所 return num; } console.log( getNum() );この場合はコンソールに5が表示される。これはまだ直感的だろう。
ではこれはどうだろう?
javascript
// 外側のnumのスコープここから
let num = 10;
const getNum = () {
// getNum()内のnumのスコープここから
console.log(num) // 変更箇所
let num = 5;
return num;
// getNum()内のnumのスコープここまで
}
console.log( getNum() );
// 外側のnumのスコープここまで
まずこの場合のconsole.log(num)
はlet num = 10;
とlet num = 5
のどちらを参照するか?
正解はlet num = 5
だ。この現象が「let による巻き上げ」だ。numがどこで宣言されていようと、numはスコープ全体を消費する。
(巻き上げ自体はvarでも同様のことが言える。varの場合は関数の単位でスコープ全体を消費する)上のコードの場合もう1つ問題が発生する。宣言の前にnumが参照されるとどうなるか?とということだ。
具体的にはconsole.log(num)
がlet num = 5
よりも前に存在している。この場合
console.log(num)
はエラーが発生する。letで宣言される関数が、宣言される前にスコープ内で実際にアクセスされた場合は、参照エラーとなる。
varの場合は参照エラーにはならないが値は常に未定義となる。なおこのようなエラーが発生する領域をTDZ(Temporal Dead Zone)と呼ぶ。ifを絡めた面倒な話
これにifを絡めるとやや面倒な話になる。
let num = 0; function getNum() { if (!num) { let num = 1; } return num; } console.log( getNum() );この場合コンソールに表示されるのは、1...ではなく0になる。
以下のようにスコープを明示すると分かりやすくなる。let num = 0; function getNum() { if (!num) { // このnumは(スコープチェーンにより"let num = 0;"を参照) // if内のブロックのスコープここから let num = 1; // if内のブロックのスコープここまで } return num; } console.log( getNum() );
let num = 1
はif内のブロックでのみ有効であり、ifの条件式におけるnum
&return num;
はlet num = 0;
を参照している。
言われれば当たり前のことだが、慣れないと引っかかりそうな話ではある。
- 投稿日:2020-01-13T17:18:23+09:00
JavaScript勉強の記録その18: 数値の操作ができるメソッドまとめ
floorメソッド
index.jslet avg = 7.3333; console.log(Math.floor(avg)); //切り下げ //=>7ceilメソッド
index.jslet avg = 7.3333; console.log(Math.ceil(avg)); //切り上げ //=>8roundメソッド
index.jslet avg = 7.3333; console.log(Math.round(avg)); //四捨五入 //=>7toFixedメソッド
index.jslet avg = 7.3333; console.log(avg.toFixed(3)); //少数点第3位まで表示 //=>7.333 console.log(avg.toFixed(2)); //少数点第2位まで表示 //=>7.33randomメソッド
index.jslet avg = 7.3333; console.log(Math.random()); //0~1までのランダムの数値を生成 //=>0.49170194909693343
- 投稿日:2020-01-13T17:17:36+09:00
JavaScriptの変数再代入でやっちまったこと
今回の原因
PHPをよく使っているのですが、
PHPの場合変数の宣言と代入はこのようにします。
これが今回のイージーミスに繋がったと言い訳しておきます。name.php$hoge = '犬'; // hogeという変数に「犬」という文字列が入りました。 echo $hoge; // 犬 $hoge = '猫'; // さっき宣言した変数hogeに、値が代入され「猫」に変わりました。 echo $hoge; // 猫JavaScriptの変数宣言・代入
JavaScriptの場合の変数宣言と代入はこうです。
name.jslet hoge = '犬'; // hogeという変数に「犬」という文字列が入りました。JavaScriptの変数への再代入
そして代入をしてみます。
間違い
name.jslet hoge = '犬'; // 宣言・代入 let hoge = '猫'; // 間違い。代入されません。 console.log(hoge) // hoge is not defined正しい
name.jslet hoge = '犬'; // 宣言・代入 hoge = '猫'; // 正しい。 console.log(hoge) // 猫反省
PHPだと変数の前には'$'をつけるので、JavaScriptでもうっかりつけてしまって、
「ファッッ?! 挙動がおかしい!!!」
となりました。気をつけます。
- 投稿日:2020-01-13T17:17:36+09:00
JavaScriptの変数代入でやっちまったこと
今回の原因
PHPをよく使っているのですが、
PHPの場合変数の宣言と代入はこのようにします。
これが今回のイージーミスに繋がったと言い訳しておきます。name.php$hoge = '犬'; // hogeという変数に「犬」という文字列が入りました。 echo $hoge; // 犬 $hoge = '猫'; // さっき宣言した変数hogeに、値が代入され「猫」に変わりました。 echo $hoge; // 猫JavaScriptの変数宣言
JavaScriptの場合の変数宣言はこうです。
name.jslet hoge = '犬'; // hogeという変数に「犬」という文字列が入りました。JavaScriptの変数への代入
そして代入をしてみます。
間違い
name.jslet hoge = '犬'; // 宣言 let hoge = '猫'; // 間違い。代入されません。 console.log(hoge) // 犬正しい
name.jslet hoge = '犬'; // 宣言 hoge = '猫'; // 正しい。 console.log(hoge) // 猫反省
PHPだと変数の前には'$'をつけるので、JavaScriptでもうっかりつけてしまって、
「ファッッ?! 挙動がおかしい!!!」
となりました。気をつけます。
- 投稿日:2020-01-13T17:15:31+09:00
【Nuxt.js】Validation基礎編:フォームバリデーション
前置き
正規表現を使ってフォームバリデーション!
今回はメールアドレス形式
hoge@hoge.hogeのみ送信可能にします?
間違った形式で送られることがないので
トラブル防止などにもなり便利ですね♪初期準備
今回はコンポーネントは使わず
index.vueの1ページに全て書きます✍️
まずはFormタグと送信ボタンを用意。
基本的なフォーム構成は別記事で解説済み?
◾️【Nuxt.js】v-model実践編:オリジナルフォームの簡単な作り方
https://note.com/aliz/n/n5b9bd618399e{{ Validation.result }}
入力した値それぞれに対応したテキストを表示。
・空の場合
・バリデーションしてNGの場合
・バリデーションしてOKの場合index.vue<template> <div class="page"> <p>{{ Validation.result }}</p> <form @submit.prevent> <label>Mail <input type="text" placeholder="メールアドレスを入力" v-model="mail" > </label> </form> <button type="submit" @click="checkForm"> 送信 </button> </div> </template> <script> export default { data () { return { mail: "", Validation:{ result: "", }, } }, } </script>methodsを追加(正規表現)
【基礎文法】
Vueの公式ページをご確認ください。
https://jp.vuejs.org/v2/cookbook/form-validation.html#カスタムバリデーションの利用index.vuemethods名: function (引数名) { var 変数名 = 正規表現; return 変数名.test(引数名); }【コード】
引数名は分かりやすくinputdataに、
変数名は正規表現を英語にしたregexに。
正規表現についてはこちらで解説しています。
◾️【Nuxt.js】正規表現基礎編①:よく使う表現を単語分割で解説!
https://note.com/aliz/n/n898319c9042dindex.vue<script> export default { data () { return { mail: "", Validation:{ result: "", }, } }, methods:{ checkString (inputdata){ var regex = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; return regex.test(inputdata); } } } </script>methodsを追加(送信可不可)
【構成】
送信ボタンを押すとcheckForm発動。
2つに分けて考えます。
これを分けるために変数mailBoolの
真偽値によってパターン分けしています?
◾️空入力&形式が間違った場合
mailBool = false
◾️形式が正しい場合
mailBool = true【コード】
index.vue<script> export default { data () { return { mail: "", Validation:{ result: "", }, } }, methods:{ checkForm() { var mailBool = false if (!this.mail) { this.Validation.result="入力してください" } else if (!this.checkString(this.mail)){ this.Validation.result="メールアドレス形式で入力してください" } else { mailBool = true } if(mailBool === true){ this.Validation.result="送信に成功しました!" alert(this.mail + 'で送信しました。'); console.log(this.mail); this.mail = ""; } }, checkString (mail){ var regex = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; return regex.test(mail); } } } </script>【解説】
◾️空入力&形式が間違った場合
mailBool = false
・if (!this.mail)
data内mail=inputの入力がfalse(空)なら
該当テキストを表示
・else if (!this.checkString(this.mail))
空ではないが、
checkString関数でthis.mailを
バリデーションしてfalseなら
該当テキストを表示
・else { mailBool = true }
どちらでもなく正しく入力された場合
mailBool = trueにする◾️形式が正しい場合
mailBool = true
・this.mail = "";
送信後はメールアドレスを
残したままにせず空に戻す全体コード
index.vue<template> <div class="page"> <p>{{ Validation.result }}</p> <form @submit.prevent> <label>Mail <input type="text" placeholder="メールアドレスを入力" v-model="mail" > </label> </form> <button type="submit" @click="checkForm"> 送信 </button> </div> </template> <script> export default { data () { return { mail: null, Validation:{ result: "", }, } }, methods:{ checkForm() { var mailBool = false if (!this.mail) { this.Validation.result="入力してください" } else if (!this.checkString(this.mail)){ this.Validation.result="メールアドレス形式で入力してください" } else { mailBool = true } if(mailBool === true){ this.Validation.result="送信に成功しました!" alert(this.mail + 'で送信しました。'); console.log(this.mail); this.mail = ""; } }, checkString (inputdata){ var regex = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; return regex.test(inputdata); } } } </script> <style lang="scss" scoped> .page { padding: 50px 20px; p { font-size: 24px; position: absolute; top: 10px; } label { display: block; margin-top: 5px; } } </style>
- 投稿日:2020-01-13T17:07:22+09:00
JSer向け dart配列API
List関連の関数
https://api.dart.dev/stable/2.7.0/dart-core/List-class.htmlArray.prototype.slice()
start-endの配列を抽出
var testList = [1, 2, 3]; testList.sublist(0,2); // => [1, 2]Array.prototype.filter()
条件にあうものを抽出
var testList = [1, 2, 3]; testList.where((int item) => item >= 2).toList(); // => [2, 3]Array.prototype.map()
mapで値を変換して返す
var testList = [1, 2, 3]; testList.map((int item) => Text(item)).toList(); // => [Text("2"), Text("3")]Array.prototype.every()
全ての条件がtrue であれば true
var testList = [1, 2, 3]; testList.every((int item) => item >= 0).toList(); // => true testList.every((int item) => item >= 2).toList(); // => falseArray.prototype.some()
1つの条件がtrueであれば true
var testList = [1, 2, 3]; testList..any((item) => item >= 2); // => true testList..any((item) => item >= 4); // => false
- 投稿日:2020-01-13T17:03:07+09:00
JavaScriptの「継承」はどう定義されるのか? 仕様書を読んで理解する
継承は、クラスベースのオブジェクト指向における基本的な概念のひとつであると信じられています。JavaScriptにもES2015以降は
class
構文があり、extends
を用いてクラスの継承を記述することができます。また、それより以前もprototype
を通じてオブジェクト指向的なプログラムが書かれてきました。この記事では、JavaScriptにおける「継承」がどのようなものであり、どのように定義されるのかを解説します。タイトルにある通り、今回はECMAScript仕様書に対する解説を中心とします。
仕様書はJavaScriptというプログラミング言語がどのようなものかを定義する文書であり、あなたが書いたJavaScriptプログラムは仕様書に書かれた通りの動きをすることになります1。したがって、たとえあなたが自分自身で書いたものだったとしても、JavaScriptプログラムの意味を完全な確信を持って理解したいならば、仕様書を読んで理解することがその唯一の手段なのです。
今回は継承というトピックを取り上げつつ、仕様書がJavaScriptという言語をどのように定義するのかの一端を理解するための、仕様書入門のような記事を目指しました。仕様書というだけで臆することなく、この記事を頼りに果敢に仕様書リーディングに挑戦してみましょう。
ウォーミングアップ:
prototype
と継承仕様書を読み始める前に、JavaScriptにおいて継承という機構がそもそもどのように実現されているのかを解説しておきます。もう知っているという方は次の節に進んでも構いません。
インスタンスの作成と
instanceof
JavaScriptには
instanceof
という演算子があります。これは、あるオブジェクトがあるクラスのインスタンスであるかどうかを判定する演算子です。class MyClass {} const normalObj = {}; const myClassObj = new MyClass(); console.log(normalObj instanceof MyClass); // false console.log(myClassObj instanceof MyClass); // true逆に、あるクラスのインスタンスを作る方法は、このプログラムにもあるように
new
を使うことです。なお、上のプログラムでは
class
構文を用いてクラスを宣言しましたが、これはES2015以降で使用できる方法です。それ以前はただの関数をクラスとして扱っていました。この関数はコンストラクタとして扱われ、new
されるとその関数が呼ばれます。function MyClass() {} const myClassObj = new MyClass(); console.log(myClassObj instanceof MyClass); // true
prototype
によるインスタンスの特徴付けクラス定義にはメソッドを含めることができます。クラス定義に書かれたメソッドは、当然ながらインスタンスから利用可能です。しかしながらhasOwnPropertyメソッドで調べると、インスタンスにそんな名前のプロパティは無いという結果になります。
class MyClass { method() { console.log("hi"); } } const obj = new MyClass(); obj.method(); // "hi" と表示される console.log(obj.hasOwnProperty("method")); // false と表示されるこれはやや難しいところですが、我々にオブジェクトのプロパティ・メソッドとして見えるものは2種類あります。ひとつはオブジェクト自身のプロパティ、もうひとつはオブジェクトのプロトタイプ由来のプロパティです。
{ foo: 123 }
とかobj.bar = 456
といった方法で宣言されるのはオブジェクト自身のプロパティである一方、クラスにて宣言されたメソッドはプロトタイプ由来のプロパティとなります。そして、hasOwnProperty
は前者に対してのみtrue
を返すのです。つまるところ、JavaScriptのオブジェクトは「連想配列 + プロトタイプ」として説明できます2。そして、JavaScriptのオブジェクト指向的側面を支えるのがプロトタイプなのです。昔を知っている方は「
prototype
なんでES2015が出てお役御免になったでしょ?」とお思いかもしれませんが、class
構文の裏を支えるのもやはりプロトタイプの機構です。オブジェクトのプロトタイプは、何か別のオブジェクトです(無い場合もあります)。プロトタイプの機構は非常に単純なもので、あるオブジェクトが自身が持たないプロパティにアクセスされたとき、次にプロトタイプを探しに行くのです。
あるオブジェクトのプロトタイプを取得する手段がObject.getPrototypeOfです。
MyClass
のインスタンスの場合、そのプロトタイプはMyClass.prototype
になります。class MyClass { method() { console.log("hi"); } } const obj = new MyClass(); console.log(Object.getPrototypeOf(obj) === MyClass.prototype); // trueそして、
obj.method
として呼び出せるものは実はMyClass.prototype.method
なのです。method
はプロトタイプ由来のプロパティだったことになります。console.log(obj.method === MyClass.prototype.method); // trueプロトタイプ関連のメソッド
先ほど
Object.getPrototypeOf
を紹介しましたが、他にも関連メソッドがあります。ひとつはisPrototypeOfです。これはオブジェクトが持つプロパティ(Object.prototype
に存在)であり、自分自身が与えられたオブジェクトのプロトタイプであるかどうかを判定します。例で見ると分かりやすいでしょう。console.log(MyClass.prototype.isPrototypeOf(obj)); // trueつまるところ、これは
obj instanceof MyClass
と同じ意味となります。もうひとつはObject.setPrototypeOfです。これは、既に存在するオブジェクトのプロトタイプをあとから書き換えることができるというたいへん強力な(そして遅い)メソッドです。
例えば、ただのオブジェクトである
{}
を、Object.setPrototypeOf
を使ってあとからMyClass
のインスタンスにできます。class MyClass { method() { console.log("hi"); } } const obj = {}; console.log(obj instanceof MyClass); // false console.log(obj.method); // undefined Object.setPrototypeOf(obj, MyClass.prototype); console.log(obj instanceof MyClass); // true obj.method(); // "hi" と表示される
Object.setPrototypeOf
を使う機会は滅多に無いでしょう。MDNのページにも、こんなもの使うんじゃないぞという注意がでかでかと書いてあります。それよりも使いそうなのがObject.createです。これは新しいオブジェクトを作るときにプロトタイプを指定できるメソッドです。MyClass
のインスタンスを作りたければこうです。const obj2 = Object.create(MyClass.prototype); console.log(obj2 instanceof MyClass); // true obj2.method(); // "hi" と表示される
Object.create(MyClass.prototype)
はnew MyClass
と同様にMyClass
のインスタンスを作ることができます。違いは、後者はMyClass
のコンストラクタが呼ばれる一方で前者は呼ばれないことです。逆に言えば、new
構文は「適当なプロトタイプを持つオブジェクトを作る」「コンストラクタを呼ぶ」という2段階の工程をまとめてやってくれる親切な構文だということです。
__proto__
読者の中には
__proto__
についてご存知の方も多いでしょう。オブジェクトをコピーする系の関数の脆弱性の原因によくなっているあれです。これはオブジェクトのプロトタイプが入っているという直球なプロパティです。class MyClass { method() { console.log("hi"); } } const obj = new MyClass(); console.log(obj.__proto__ === MyClass.prototype); // true obj.method(); // "hi" obj.__proto__ = null; console.log(obj instanceof MyClass); // false console.log(obj.method); // undefinedこれは
Object.getPrototypeOf
やObject.setPrototypeOf
と同等の機能を有しています。見て分かる通り__proto__
のほうが名前が怪しいので、基本的には避けましょう3。継承
次に、継承がJavaScriptでどう扱われているかを見ましょう。まず、
instanceof
やisPrototypeOf
の挙動です。class SuperClass { method() { console.log("hey"); } } class SubClass extends SuperClass {} const obj = new SubClass(); obj.method(); // "hey" と表示される console.log(obj instanceof SubClass, obj instanceof SuperClass); // true true console.log( SubClass.prototype.isPrototypeOf(obj), SuperClass.prototype.isPrototypeOf(obj) ); // true trueこのように、
obj
はSubClass
のインスタンスですが、SbbClass
はSuperClass
を継承しているためobj
は間接的にSubClass
のインスタンスとなります。instanceof
やisPrototypeOf
はそれを認識して上記のような場合にもtrue
を返します。ところで、
SubClass
がSuperClass
を継承しているということはどのように表現されるのでしょうか。答えは、「SubClass.prototype
がSuperClass
のインスタンスである」です。確かめてみましょう。class SuperClass {} class SubClass extends SuperClass {} console.log(SubClass.prototype instanceof SuperClass); // true console.log(Object.getPrototypeOf(SubClass.prototype) === SuperClass.prototype); // true
SubClass
のインスタンスであるobj
がSuperClass
のメソッドを持っているのは次のように説明できます。すなわち、obj
自身method
という名前のプロパティを持っていないためプロトタイプ(SubClass.prototype
)に移譲されます。SubClass.prototype
はmethod
という名前のプロパティを持っていないため、次はSubClass.prototype
のプロトタイプであるSuperClass.prototype
に移譲されます。ここでmethod
が発見されます。このような移譲の連鎖によって継承という機構が実現されています。これが、プログラマのレベルから見たJavaScriptの継承です。
最上位のベースクラスとしての
Object
実は、普通のオブジェクトは
Object
のインスタンスであるということが知られています。さっそく試してみましょう。const obj = {}; console.log(obj instanceof Object); // true console.log(Object.getPrototypeOf(obj) === Object.prototype); // trueすでに何気なく登場していた
hasOwnProperty
やisPrototypeOf
も全てのオブジェクト4が持つプロパティでしたが、その実態はObject.prototype
にあります。console.log(obj.hasOwnProperty === Object.prototype.hasOwnProperty);その意味で、全てのオブジェクトは
Object
のインスタンスであると言えます。全てのクラスは暗黙のうちにObject
を継承しています。ただし、上で「全てのオブジェクト」の述べましたが、ひとつ例外があります。実はプロトタイプが無いオブジェクトを作ることができるのです。JavaScriptプログラム上では、そのようなオブジェクトはプロトタイプが
null
のオブジェクトとして現れます。例えば、プロトタイプが無いオブジェクトを新規に作るにはObject.create(null)
とすればよいことが分かります。実際にやってみると、そのようなオブジェクトは確かにObject
のインスタンスではないことが分かります。const obj = Object.create(null); console.log(obj instanceof Object); // false console.log(Object.getPrototypeOf(obj)); // null実は
Object.prototype
もまた、プロトタイプが無いオブジェクトです。Object.prototype
はその意味でプロトタイプチェーンの終端であるといえるのです。プロトタイプの機構を仕様書で追う
さて、ウォーミングアップは以上です。ここからは、前節で触れたような内容がJavaScript仕様書でどのように定義されているのか見ていきましょう。
最初は、そもそも「オブジェクトは連想配列+プロトタイプである」という概念が仕様書にどう書かれているのか見ましょう。この記事では手とり足取り解説しますのでご安心ください。ただ、実際に自分で仕様書を開いたほうがついて来やすいかもしれません。仕様書へのリンクも改めて用意しておきます。
オブジェクトの定義
仕様書を散策する手がかりは目次と全文検索が基本ですが、今回は目次を眺めていくといいですね。オブジェクトに関することが書かれていそうな箇所はいろいろありますが、最初に目に付くのは6 ECMAScript Data Types and Valuesです。この章には、そもそも「JavaScriptにおける値とは何か」に関する定義が書かれています。6.1 ECMAScript Language Types では、JavaScriptの値の種類が1つずつ定義されています。
今回はオブジェクトに興味があるので、6.1.7 The Object Typeを見ることになります。いくつかの文を引用します。以降、訳はすべて筆者によるものです。
An Object is logically a collection of properties. Each property is either a data property, or an accessor property.
(訳)オブジェクトは論理的にはプロパティの集まりです。プロパティは、データプロパティかアクセサプロパティのどちらかです。
logically(論理的には)という言葉の意味がつかめない人もいるかもしれません。これは、仕様書では処理系における実際のメモリ配置とかそういった部分には踏み入らないことを意味しています。仕様書に書かれていることを処理系(ブラウザなど)がどう実装するかは自由であり、どんな手段であろうと仕様書に書かれている通りに動けば構わないのです。仕様書ではオブジェクトについて「プロパティの集まりである」という要件が満たされていればよく、それが実際どう実装されているのかは興味がありません。
この要件は仕様書がJavaScriptという言語を定義するための最低限の定義とも言えるものであり、あくまで実際の実装に踏み入らずに定義をベースとして言語を定めるという姿勢の現れです。このことが論理的という言葉で表現されています。
プロパティについてはさらに記載があります。
Properties are identified using key values. A property key value is either an ECMAScript String value or a Symbol value.
(訳)プロパティはキー値によって識別されます。プロパティのキー値は文字列かSymbolのどちらかです。
アクセサプロパティ(ゲッタとセッタで定義されるプロパティのことです)は置いておくとしても、「オブジェクトは連想配列である」ということがここまでで書かれていることになります。キーによって識別されるデータ(プロパティ)の集まりというのはまさに連想配列のことだからです。
次に6.1.7.2 Object Internal Methods and Internal Slotsに目を向けます。この節ではインターナルスロット(内部スロット)という概念が定義されています。インターナルスロットは、言うなれば仕様書内からしか見えないプロパティです。各オブジェクトはそれぞれインターナルスロットを持っており、仕様書のアルゴリズムはインターナルスロットを見たり書いたりすることができます。実際のプログラムからはインターナルスロットは不可視です。
また、インターナルメソッドというものもあり、これはインターナルスロットの関数版です。この節ではインターナルメソッドの定義が中心です。なお、ここでessential internal methods(必須インターナルメソッド)についても定義されています。これは全てのオブジェクトが持つインターナルメソッドのことです。
お察しの通り、オブジェクトのもうひとつの特徴である「プロトタイプを持つ」という点はこのインターナルスロットによって表現されます。しかし、ここまで見てきた箇所を探してもプロトタイプに関する記載はありません。そもそもここで具体的に定義されているのは必須インターナルメソッドだけです。
ということで再び目次を眺めると、9.1 Ordinary Object Internal Methods and Internal Slotsが関係ありそうです。以下に関連部分を引用します。なお、文中に出てくるordinaryオブジェクトというのは普通のオブジェクトのことで、我々が普段扱うオブジェクトは大抵がordinaryオブジェクトです。
All ordinary objects have an internal slot called [[Prototype]]. The value of this internal slot is either null or an object and is used for implementing inheritance.
(訳)全てのordinaryオブジェクトは[[Prototype]]というインターナルスロットを持ちます。このインターナルスロットの値はnullまたはオブジェクトであり、継承を実装するのに用いられています。
このように、インターナルスロットは
[[ ]]
で囲われた名前を持ちます。この文には[[Prototype]]について「継承を実装するために用いられる」としかありませんので、これまで説明したような機構が具体的にどう実装されているのかを理解するにはさらに仕様書を読み進める必要があります。
Object.create
の定義を読む手始めに、
Object.create
がどう定義されているのか、仕様書を読み解いてみましょう。このメソッドは、指定されたプロトタイプでオブジェクトを作るだけという簡単な挙動なので定義も比較的読みやすいものとなっています。仕様書の目次からそれっぽいところを探すと、
Object.create
の定義は19.1.2.2 Object.create(O, Properties)に見つかります。定義はやはり短いですね。The create function creates a new object with a specified prototype. When the create function is called, the following steps are taken:
- If Type(O) is neither Object nor Null, throw a TypeError exception.
- Let obj be ObjectCreate(O).
- If Properties is not undefined, then
a. Return ? ObjectDefineProperties(obj, Properties).- Return obj.
(訳)create関数は与えられたプロトタイプを持つ新しいオブジェクトを作成します。create関数が呼ばれたとき、次の手順が実行されます。
- もし Type(O) が Object でも Null でもなければ、TypeError例外を発生させる。
- objをObjectCreate(O)とする。
- もしPropertiesがundefined以外なら、
a. ? ObjectDefineProperties(obj, Properties)を返す。- objを返す。
このように、組み込み関数の定義は案外直感的です。関数の動作がこのように自然言語で書かれています。また、Type, ObjectCreate, ObjectDefinePropertiesという別の関数の呼び出しが含まれています。これらは abstract operation(抽象操作?)と呼ばれ、仕様書内で定義された関数です。abstract operationはランタイムに利用できる何らかの組み込み関数に対応するものではなく、仕様書内でのみ参照・利用されるものです。
また、ステップ3-aに?という記法があります。これは5.2.3.4 ReturnIfAbrupt Shorthandsで定義されているものであり、簡単に言えば「抽象操作でエラーが発生したらそのエラーを伝播させる」という挙動を表す省略記法です。JavaScriptプログラムでは発生したエラーは自動的に伝播しますが、仕様書のアルゴリズムのレベルでは明示的に伝播させなければいけません。この操作は頻出なので、?という短い記法が用意されているのです。また、?のほかに!という種類があり、これはその操作が失敗しないことを表すものです5。
さて、
Object.create
の定義を読むと分かる通り、処理の本体はObjectCreateという抽象操作にあるようです。次はこちらを読みましょう。多くの定義はこのように、通常の言葉で書かれた説明と、番号付きリストで書かれたアルゴリズム部分から成ります。リスト部分だけ見れば動作はちゃんと定義されていますが、読み手の分かりやすさのために最初の説明が書かれています。
The abstract operation ObjectCreate with argument proto (an object or null) is used to specify the runtime creation of new ordinary objects. The optional argument internalSlotsList is a List of the names of additional internal slots that must be defined as part of the object. If the list is not provided, a new empty List is used. This abstract operation performs the following steps:
- If internalSlotsList is not present, set internalSlotsList to a new empty List.
- Let obj be a newly created object with an internal slot for each name in internalSlotsList.
- Set obj's essential internal methods to the default ordinary object definitions specified in 9.1.
- Set obj.[[Prototype]] to proto.
- Set obj.[[Extensible]] to true.
- Return obj.
(訳)抽象操作ObjectCreateは引数proto(オブジェクトまたはnullである)を取り、新しいordinaryオブジェクトをランタイムで作るために使用されます。オプショナル引数internalSlotsListは、作成されるオブジェクトに対して定義されなければならない追加のインターナルスロットのリストです。このリストが渡されなかった場合は、空リストが用いられます。この抽象操作は次の操作を実行します。
- internalSlotsListが存在しない場合は、新しい空リストとする。
- internalSlotsListで示されたそれぞれのインターナルスロット名を備えた新しいオブジェクトを作り、それをobjとする。
- objの必須インターナルメソッドを9.1で定義されたデフォルトの内容で作成する。
- obj.[[Prototype]]をprotoとする。
- obj.[[Extensible]]をtrueにする。
- objを返す。
読むと分かる通り、「新しいオブジェクトを作成し、必要なインターナルスロットを用意する」という内容です。ObjectCreateは普通のオブジェクトを作る操作なので、インターナルメソッドの動作はデフォルトの内容です。そして、問題の[[Prototype]]インターナルスロットもちゃんとここでセットされています。
このように、インターナルスロットはちゃんとオブジェクトが作成されるたびに明示的に用意されています。たいへん健気ですね。そもそも「結局オブジェクトを作るってどういうことなの?」と思った方もいるかもしれませんが、それについては仕様書の守備範囲ではありません。そこに踏み込まなくても言語は定義できるからです。
Object.getPrototypeOf
の定義も見てみるでは、次に
Object.getPrototypeOf
の定義はどうなっているか見てみましょう。とはいっても、ここまで読んだ皆さんはその定義がどうなっているか容易に想像できるでしょう。与えられたオブジェクトの[[Prototype]]インターナルスロットの値を返せばいいのです。19.1.2.12 Object.getPrototypeOf(O)
When the getPrototypeOf function is called with argument O, the following steps are taken:
- Let obj be ? ToObject(O).
- Return ? obj.[[GetPrototypeOf]]().
(訳)getPrototypeOf関数が引数Oで呼ばれたとき、次の手順が実行されます。
- objをToObject(O)とする。
- obj.[[GetPrototypeOf]]()を返す。
実際の定義は非常に短いですが、どうも一筋縄ではいかないようです。最初のToObjectは与えられた値がオブジェクト以外だったらオブジェクトに変換する抽象操作です(nullとundefiendはエラーになります)。
問題は2で、実際にプロトタイプを返す処理が[[GetPrototypeOf]]インターナルメソッドに移譲されています。その理由はProxyの存在です。Proxyは「プロトタイプを取得する」という操作に対してカスタムされた挙動を定義することができます。このことを、仕様書では「[[GetPrototypeOf]]インターナルメソッドの挙動が異なる」という形で定義しているのです。この記事では触れませんが、Proxyの定義を読むとそのようなことが書いてあります。
では、今回は普通のオブジェクトを相手しているので、普通のオブジェクトの[[GetPrototypeOf]]の挙動を追いましょう。Object.createの定義を読んだときに見たように、普通のオブジェクトに対する必須インターナルメソッドの定義は9.1に書かれています。[[GetPrototypeOf]]の定義はこうです。
9.1.1 [[GetPrototypeOf]]()
When the [[GetPrototypeOf]] internal method of O is called, the following steps are taken:
- Return ! OrdinaryGetPrototypeOf(O).
9.1.1.1 OrdinaryGetPrototypeOf(O)
When the abstract operation OrdinaryGetPrototypeOf is called with Object O, the following steps are taken:
- Return O.[[Prototype]].
(訳)Oの[[GetPrototypeOf]]内部メソッドが呼ばれた場合、次の操作が実行されます。
1. ! OrdinaryGetPrototypeOf(O)を返す。9.1.1.1 OrdinaryGetPrototypeOf(O)
抽象操作OrdinaryGetPrototypeOfがオブジェクトOで呼ばれた場合、次の操作が実行されます。
- O.[[Prototype]]を返す。
ということで、やっと[[Prototype]]が登場しました。ここに書いてある通り、普通のオブジェクトは[[GetPrototypeOf]]()が呼ばれたらそのオブジェクトの[[Prototype]]内部スロットの値が返されます。これで
Object.getPrototypeOf
の説明がつきましたね。プロパティアクセスの定義を見る
次は、プロトタイプによる継承機構の本体とも言えるプロパティアクセス(
obj.foo
)の定義を見てみます。ここには、無かったらプロトタイプを辿るという挙動が定義されているはずです。今回は構文定義がスタート地点となります。構文の定義はいくつかの章にまとまっており、obj.foo
は式なので12 ECMAScript Language: Expressionsの中にあります。具体的には12.3.2.1 Runtime Semantics: Evaluationです。MemberExpression . Identifier の定義
以下は MemberExpression . Identifier という構文の実行の定義です。
MemberExpression: MemberExpression.IdentifierName
- Let baseReference be the result of evaluating MemberExpression.
- Let baseValue be ? GetValue(baseReference).
- Let bv be ? RequireObjectCoercible(baseValue).
- Let propertyNameString be StringValue of IdentifierName.
- If the code matched by this MemberExpression is strict mode code, let strict be true, else let strict be false.
- Return a value of type Reference whose base value component is bv, whose referenced name component is propertyNameString, and whose strict reference flag is strict.
(訳)
1. MemberExpressionを実行し、その結果をbaseReferenceとする。
2. baseValueを? GetValue(baseReference)とする。
3. bvを? RequireObjectCoercible(baseValue)とする。
4. propertyNameStringをIdentifierNameのStringValueとする。
5. もしこのMemberExpressionがstrictモードのコードならstrictをtrueとし、それ以外ならstrictをfalseとする。
6. base value componentがbvであり、refereced name componentがpropertyNameStringであり、strict reference flagがstrictであるようなReference型の値を返す。これを読むと、ちょっと様子がおかしいですね。最後の行を見ると「Referenceを返す」というよく分からないことが書いてあります。Referenceとは仕様書内でのみ用いられる値で、プロパティ(または変数)へのアクセスそのものを表す特殊な値です。6.2.4 The Reference Specification Typeには以下のように記載されています。
A Reference is a resolved name or property binding. A Reference consists of three components, the base value component, the referenced name component, and the Boolean-valued strict reference flag. The base value component is either undefined, an Object, a Boolean, a String, a Symbol, a Number, or an Environment Record. A base value component of undefined indicates that the Reference could not be resolved to a binding. The referenced name component is a String or Symbol value.
(訳)Referenceは変数またはプロパティに対する解決済のバインディングです。Referenceはbase value component, referenced name component, そして真偽値を値に持つstrict reference flagという3つの構成要素から成ります。base value componentはundefinedか、真偽値、文字列、シンボル、数値またはEnvironment Recordです。base value componentがundefinedの場合、そのReferenceがバインディングに解決できなかったことを表します。referenced name componentは文字列またはシンボルです。
バインディングというのは、変数やプロパティの中身ではなく、変数・プロパティそれ自身を指す言葉です。要するに、
obj.foo
と書いた段階ではまだ実際にobj
からfoo
というプロパティが取り出されているわけではないのです。代わりに、「obj
のfoo
にアクセスする」という情報がそのまま入ったReferenceが返されるのです。Referenceがどのように活躍するかについては筆者の過去記事でも扱っています。詳細はそちらに譲りますが、参照の機構は代入などを仕様化する際に役立っています。代入の際は「どのオブジェクトのどのプロパティに代入するか」という情報を取り回す必要があり、それがまさにReferenceです。
さて、上の定義で行われているのはReferenceを作ることだけであり、実際に参照を解決して
obj.foo
の値を得るのは別の箇所で行われます。具体的にはこれを行うのはGetValueです。このGetValueは非常に出番の多い抽象操作です(上の定義にも出てきていますね)。値が欲しいのに参照が渡されるかもしれない画面では、参照を値に解決するためにGetValueが用いられます。GetValueの定義を読む
GetValue(V)の定義を以下に引用します。訳すほど複雑なことは書いていないので日本語訳は省略します。
- ReturnIfAbrupt(V).
- If Type(V) is not Reference, return V.
- Let base be GetBase(V).
- If IsUnresolvableReference(V) is true, throw a ReferenceError exception.
- If IsPropertyReference(V) is true, then
a. If HasPrimitiveBase(V) is true, then
i. Assert: In this case, base will never be undefined or null.
ii. Set base to ! ToObject(base).
b. Return ? base.[[Get]](GetReferencedName(V), GetThisValue(V)).- Else base must be an Environment Record,
a. Return ? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V)) (see 8.1.1).1と2は、渡された値Vが参照以外だった場合の処理です。参照以外は何もせずにそのまま返されます。
3はGetBaseを呼び出していますが、これはVのbase value componentを取得するだけです。もし
obj.foo
を表すReferenceなら、base value componentはobj
になります。4はIsUnresolvableReference(V)ならばReferenceErrorを発生させます。これは、Vが「未定義の変数」への参照だった場合のことです。プログラム中で未定義の変数にアクセスした場合にエラーが発生するのはよく知られていますが、そのエラーはこのGetValueから発生しているのです。
5は、参照がプロパティへの参照か、それともただの変数への参照かによって分岐しています。今回はプロパティへの参照に興味があるのでifの中を見ましょう。結局のところ、よく見ると処理はbase.[[Get]]に移譲されていることが分かります。何やら先が長いですね。
[[Get]]の定義を読む
[[Get]]は見て分かるとおり内部メソッドであり、プロパティアクセスの処理を定義するものです。今回はやはり普通のオブジェクトにおける挙動を見ます。9.1.8 [[Get]](P, Receiver)です。なお、これはOrdinaryGet(O, P, Receiver)に移譲されています。
When the abstract operation OrdinaryGet is called with Object O, property key P, and ECMAScript language value Receiver, the following steps are taken:
- Assert: IsPropertyKey(P) is true.
- Let desc be ? O.[[GetOwnProperty]](P).
- If desc is undefined, then
a. Let parent be ? O.[[GetPrototypeOf]]().
b. If parent is null, return undefined.
c. Return ? parent.[[Get]](P, Receiver).- If IsDataDescriptor(desc) is true, return desc.[[Value]].
- Assert: IsAccessorDescriptor(desc) is true.
- Let getter be desc.[[Get]].
- If getter is undefined, return undefined.
- Return ? Call(getter, Receiver).
1はAssertと書いてありますが、これはその地点で必ず満たされている条件を宣言するものです。仕様書は気をつけて書かれているため、Assertに反する状況に陥ることはありません(仕様書にバグが無ければ)。Assertは読み手が理解しやすいように提供されているものです。
2で[[GetOwnProperty]]という別の内部メソッドが使われています。深追いするのはやめておきますが、これはそのオブジェクトの与えられた名前のプロパティのプロパティデスクリプタを返す内部メソッドです。もしオブジェクトがそのプロパティを持たなければundefinedが返されます。
3の分岐は[[GetOwnProperty]]がundefinedを返した場合、つまりオブジェクトがそのプロパティを持っていなかった場合の処理です。最初に説明した通り、この場合にプロトタイプを辿るという動作が発生することになります。3の中身を見ると、まず[[GetPrototypeOf]]でOのプロトタイプを取得しています(少し前に確認した通り、普通のオブジェクトに対する[[GetPrototypeOf]]の挙動はそのオブジェクトの[[Prototype]]内部スロットの値を返すだけです)。ここではプロトタイプはparentという名前がついた変数に保存されています。
parnetがnullのとき、すなわちプロトタイプが無かった場合はundefinedを返すとあります。これが、「オブジェクトの存在しないプロパティにアクセスしたらundefiedが返る」という挙動を定義している箇所です。プロトタイプチェーンの末端(Object.prototype)まで見ても見つからなかった場合に最終的にここに行き着きます。
プロトタイプがあった場合はプロトタイプに処理が移譲されます。それを表すのが parnet.[[Get]](P, Receiver) という部分です。
4はオブジェクト自身がデータプロパティを持っていた場合にその値を返すという処理で、5以降はアクセサプロパティがあった場合の処理です。
ともかく、これでプロトタイプチェーンの機構を仕様書で確認することができました。「オブジェクト自身のプロパティをチェックし、無ければプロトタイプを見に行く」という処理が、普通のオブジェクトの[[Get]]内部メソッドの定義にほぼそのままの形で記述されていましたね。これは再帰になっているため、プロトタイプチェーンが長く連なっている場合にも正しく処理されます。
組み込みオブジェクトの継承構造
おまけ的な話題として、組み込みのオブジェクトの間の継承構造がどのように定義されているのかを見てみます。
普通のオブジェクトのプロトタイプ
我々がオブジェクトを作るとき、最も一般的なのは
{}
のようなオブジェクトリテラルを使って作る方法です。この方法で作られるのは普通のオブジェクトであり、しかも自動的にObject.prototype
をプロトタイプに持っています。このことは仕様書でどう定義されているのでしょうか。今回は
{}
の挙動を調べたいので、オブジェクトリテラルを定義している部分を探しましょう。オブジェクトリテラルが式の一種であることを理解していれば探すのは難しくなく、目次のありそうな部分を眺めれば12.2.6 Object Initializerを見つけるのは容易いでしょう。文や式の場合は、Evaluationと書かれている部分を探しましょう。そこに実行時の挙動が定義されています。今回の場合は12.2.6.7 Runtime Semantics: Evaluationです。ObjectLiteral:{}
1. Return ObjectCreate(%ObjectPrototype%).はい、非常に単純ですね。ObjectCreateは既に出てきた抽象操作で、与えられたオブジェクトをプロトタイプとして、新しい普通のオブジェクトを作るものです。今回プロトタイプとして指定されているのは%ObjectPrototype%だそうです。
ここで何やら新しい記法が出てきました。このように% %で囲まれた名前はintrinsic objectと呼ばれ、要するに仕様書内で通用するグローバル変数のようなものです(書き換えられることはないので定数と呼ぶべきかもしれませんが)。仕様書のあちこちで使われるオブジェクトはこのようにintrinsic object(内部オブジェクト?)として定義され、仕様書内で簡単に参照できるようになっています。
Object.prototype
に相当するオブジェクトは仕様書内でよく使われるので、%ObjectPrototype%として簡単に参照できるようになっています。%ObjectPrototype%がどんなオブジェクトかということは、19.1.3 Properties of the Object Prototype Objectで定義されています。
The Object prototype object:
- is the intrinsic object %ObjectPrototype%.
- is an immutable prototype exotic object.
- has a [[Prototype]] internal slot whose value is null.
(訳) Object prototypeオブジェクトは、
1. 内部オブジェクト%ObjectPrototype%です。
2. immutable prototypeエキゾチックオブジェクトです。
3. [[Prototype]]内部スロットを持ち、その値はnullです。この節では、「Object prptotypeオブジェクトというオブジェクトが存在する」ということを定義しています。つまり、ある種のオブジェクト作成の定義になっています。実際にこのオブジェクトを作るのは、もちろんJavaScript処理系が実行環境の準備中に勝手にやってくれます。オブジェクトを作成する以上、必要な内部スロットはちゃんと明示的に作ってあげなければいけません。そのため、[[Prototype]]内部スロットの存在及びその中身がここに明記されています。
Object prototypeオブジェクトはimmutable prototypeエキゾチックオブジェクトであるとされていますが、これの定義は9.4.7 Immutable Prototype Exotic Objectsにあります。
An immutable prototype exotic object is an exotic object that has a [[Prototype]] internal slot that will not change once it is initialized.
Immutable prototype exotic objects have the same internal slots as ordinary objects. They are exotic only in the following internal methods. All other internal methods of immutable prototype exotic objects that are not explicitly defined below are instead defined as in ordinary objects.
(訳)immutable prototypeエキゾチックオブジェクトは、作られた後に[[Prototype]]内部スロットが変更されないようなエキゾチックオブジェクトです。
Immutable prototypeエキゾチックオブジェクトは普通のオブジェクトと同じ内部スロットを持ちます。Immutable prototypeエキゾチックオブジェクトは次の内部メソッド(訳注:[[SetPrototypeOf]])のみが普通のオブジェクトと異なります。以下で明示的に定義されていない他の内部メソッドは、普通のオブジェクトと同様に定義されます。
目ざとい方は先ほどの%ObjectPrototype%の定義について「必須インターナルスロットの定義がないじゃないか」とお思いになったかもしれませんが、上記に「以下で明示的に定義されていない他の内部メソッドは、普通のオブジェクトと同様に定義されます」という文があるためクリアしていると考えられます。
エキゾチックオブジェクトというのは、内部メソッドの動作が普通のオブジェクトと事なるオブジェクトのことであり、いくつか種類があります。そのうちのひとつであるimmutable prototypeエキゾチックオブジェクトは、[[SetPrototypeOf]]という内部メソッドの動作が普通のオブジェクトと違います。
[[SetPrototypeOf]]は
Object.setPrototypeOf
経由で呼び出される内部メソッドであり、要するにオブジェクトのプロトタイプをあとから変更するためのものです。immutable prototypeエキゾチックオブジェクトはこれを許可せず、[[SetPrototypeOf]]の挙動を何もしないように変更することで、プロトタイプの書き換えを防いでいます。Object prototypeオブジェクトがimmutable prototypeエキゾチックオブジェクトとして定義されていることにより、
Object.prototyppe
のプロトタイプはnull
に固定されています。実際、以下のようにこれを書き換えようとするとエラーが発生します。const p = Object.create(null); Object.setPrototypeOf(Object.prototype, p); // エラーメッセージ(Google Chromeの場合): // Uncaught TypeError: Immutable prototype object '#<Object>' cannot have their prototype set話がそれましたが、このように仕様書内で存在が定義されているオブジェクトというのは数多くあります。一部はintrinsic objectとして名前が付けられており、6.1.7.4 Well-Known Intrinsic Objectsにそれが列挙してあります。現在のところこれは111種類あります。
組み込みオブジェクトの継承関係:配列の場合
最後に、組み込みオブジェクトの継承関係を見ておきましょう。例えば、ArrayはObjectを継承しているため、Objectのメソッドは配列に対して使用することができます。
const arr = [1, 10, 100]; console.log(arr.hasOwnProperty("1")); // trueこれは仕様書でどのように定義されているでしょうか。とはいっても、話は難しくありません。すでにご存知の通り、「ArrayがObjectを継承している」というのは「
Array.prototype
がObjectのインスタンスである」ということ、言い換えれば「Array.prototype
のプロトタイプがObject.prototype
である」ということです。これを確かめればいいわけです。ここまで読んだ皆さんなら、仕様書の「
Array.prototype
というオブジェクト」を定義している部分を読めばいいというのはすぐに分かるでしょう。早速目次から探しましょう。すると、22.1.3 Properties of the Array Prototype Objectが見つかります。Properties ofとあるタイトルは若干ミスリーディングですが、Array Prototype Objectそのものもここで定義されています。
The Array prototype object:
- is the intrinsic object %ArrayPrototype%.
- is an Array exotic object and has the internal methods specified for such objects.
- has a "length" property whose initial value is 0 and whose attributes are { [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false }.
- has a [[Prototype]] internal slot whose value is the intrinsic object %ObjectPrototype%.
NOTE
The Array prototype object is specified to be an Array exotic object to ensure compatibility with ECMAScript code that was created prior to the ECMAScript 2015 specification(訳)Array prototypeオブジェクトは、
- 内部オブジェクト%ArrayPrototype%です。
- Arrayエキゾチックオブジェクトであり、対応する内部メソッドを持ちます。
- "length" プロパティを持ちます。これは0で初期化されており、[[Writable]]属性がtrue、[[Enumerable]]属性がfalse、[[Configurable]]奥世がfalseです。
- [[Prototype]]内部スロットを持ち、その値は%ObjectPrototype%です。
NOTE
Array prototypeオブジェクトはArrayエキゾチックオブジェクトであると定められていますが、これはECMAScript 2015 以前に作られたコードとの互換性を保証するためです。後方互換性の関係でいろいろと書いてありますが、4つ目の項目に
Array.prototype
の[[Prototype]]スロットの中身は%ObjectPrototype%であると明記されています。まとめ
この記事では、JavaScriptにおける継承の機構を仕様書のレベルで解説しました。[[Prototype]]内部スロットの存在や実際のプロパティアクセスの挙動を理解を通じて、JavaScriptにおけるプロトタイプベースのオブジェクト指向がどのように実現されているかを仕様書に見出しました。
仕様書の読み方については比較的丁寧に解説しましたが、アルゴリズム部分も自然言語で書かれているため読むのに必要な知識は多くありません。恐らく最も大変なのは、目的の記述を見つけることでしょう。この記事では基本的に目次から探すものとして解説しましたが、素早く目的の記述を見つけるためには仕様書のどこに何が書かれているのか把握することが重要です。仕様書の目次にひと通り目を通しておくのは効果的でしょう。
冒頭でも述べた通り、仕様書を読んで理解することがJavaScriptプログラムの意味を正確に理解するための唯一の方法です。常日頃から
class
の構文にお世話になっている方も多いと思いますが、そのインスタンスのメソッドを呼び出すということがどういうことなのか、この記事を全部読んだ方は半分くらいは理解できたかと思います(もう半分はそのときのthis
の扱いです。今回はプロトタイプの扱いに絞ったのでthis
の扱いがどうなるのかは省略しました)。仕様書を読むというのはハードルが高く感じられるかもしれませんが、ちゃんと仕様書が存在するという点でJavaScriptは幸せな部類です。仕様書さえ読めばプログラムの意味に確信が持てるというのは実はたいへんありがたいことなのです。仕様書が無い言語では、コンパイラの動作がどうなっているかにまで立ち入らなければプログラムの意味の真なる理解が達成できないかもしれないのですから。その点で、仕様書というのはプログラムの意味に関する優れた抽象化レイヤーとして働いているのです。
残念なことに、世の中の誰も彼もが自分の書いたプログラムの意味を理解しているわけではありません。それどころか、この記事の長さからも分かるように、プログラムの意味を把握するというのは決してハードルが低い行為ではありません。実際のところ、意味が分からずに書いたプログラムであってもプログラムは動いてしまいます。それが良いことなのか悪いことなのかという答えを筆者は持っていませんから、この機会に考えてみてはいかがでしょうか。
- 投稿日:2020-01-13T16:36:12+09:00
初心者向けVue.js × Railsでのアプリ作成(ToDoリスト編)
Vue.jsとRailsでアプリを作る記事が少ない!!!(泣)
私はRubyエンジニアで普段Railsを使って仕事をしているのですが、業務でVue.jsをメンテする必要があるため、空いた時間に勉強をしています。
ですが、Vue.jsとRailsでアプリを作る記事が少ない印象をうけ、勉強するのに少し不便でした。Vue.js × Railsの記事が少ないと言っても探せばそれなりに見つかるのですが、私みたいなフロントエンドの事よくわかってない人間には、理解するのに時間がかかったりします。
ネットで記事をあさったり、そもそもJavaScriptが良くわかってないので、JavaScriptから勉強し直してみたり、Vue.js × Railsでアプリを作るだけにしては非常に遠回りしてしまいました。
この記事について
この記事は、私みたいにVue.js × Railsのアプリ作成で遠回りな勉強をしている人をなくす事を目的としています。
初心者向けにVue.js × Railsでアプリを作る記事を書いて、実装のイメージを掴んでもらえれば、私のような遠回りはなくなるはず。。。1度小さいアプリを作ってしまえば、理解度がグッと上がり、他の記事も読みやすくなるのできっと大丈夫!
また、この記事は私がRubyエンジニアなので、Rubyエンジニアから見てVue.jsをどう実装しようかという視点になってます。
この記事を読む対象のレベル
Vue.js × Rails両方ともチュートリアルをやったくらいのレベルを対象としています。
Vue.jsもRailsもだいたいこんな感じというのがわかっていれば作れると思います。どんなものを作るか? Vue.js × Railsそれぞれの役割とは?
この記事では、Vue.js × RailsでミニマムなToDoリストアプリを作っていきます。定番ですね。
私はToDoアプリを作ろうとした時、Vue.js × Railsがそれぞれどんな役割をしているのかよくわからず、実装のイメージが掴めなかったのですが、いろんな記事を読んだ結果、RailsでAPIを作り、APIへのリクエストと返ってきたデータの表示をVue.jsで行うというのが多かったです。今回もこの役割分担でアプリを作っていきます。
WEB業界での経験が浅い、もしくはこれからWEB業界を目指す方はAPIのイメージがつかみにくいかもしれませんが、一言で言うとURLのリクエストを受けたら、URLに応じたデータを返すものです。
この役割のイメージが分かればRailsの部分をFirebaseに置き換えようとか、Vue.jsをReactに置き換えようとか応用が効くような気がします。
(注)私は現時点でFirebaseもReactも詳しくないので応用が効く気がするというだけ。。。詳しい方がいたら教えて下さると助かります。
完成後のイメージ
- テキストボックスにタスクを入力して追加ボタンを押すとリストに追加されて表示される。
- チェックボックスをチェックすると取り消し線が引かれる
- 削除ボタンを押すと削除
実際に作ってみよう
RailsでAPIを作る
では、実際にアプリを作ってきます。まずはRailsでAPIを作るところから。
以下のコマンドで、Railsアプリを作ります。--webpack=vue
をするとwebpackとVue.jsのインストールが出来ます。rails new todo_list --webpack=vue「webpackとかまた新しい言葉出すなよ!」って方は以下のリンクを見てください!
【5分でなんとなく理解!】Webpack入門
Webpackとは、js、cssなどフロントで作るファイルをバンドリングしてくれるものです。ToDoリストを表示するHome画面を作成
ToDoを表示するHome画面を作るため、コントローラーを作成します。
rails g controller home作成したコントローラーにindexだけ追加しておきましょう。
/app/controllers/home_controller.rbclass HomeController < ApplicationController def index end endviewからVue.jsが呼び出せるか試しますために追加します。
/app/views/home/index.erb<%= javascript_pack_tag 'hello_vue' %>routes.rbに以下を追加。
/config/routes.rbRails.application.routes.draw do root to: 'home#index' end
rails s
してlocalhost:3000
にアクセスします。
以下のような画面が表示されてればOKです。ちなみにVue.jsを変更したら
bin/webpack
で更新してあげる必要があります。(重要)APIの処理を作る
まずは、ToDoリストにタスクを追加するためにモデルを作っていきます。
rails generate model Task name:string is_done:booleanルーティングに以下を追加します。
表示用のhomeとデータを返すAPI用のapi::tasksを追加。/config/routes.rbRails.application.routes.draw do root to: 'home#index' namespace :api, format: 'json' do resources :tasks, only: [:index, :create, :destroy, :update] end endリクエストを受けたらデータを返すため、Tasksのコントローラーを作ります。
rails g controller Api::Tasksマイグレーションします。
rails db:migrateコントローラーの中身は以下のような感じ。
/app/controllers/api/tasks_controller.rbmodule Api class TasksController < ApplicationController skip_before_action :verify_authenticity_token def index @tasks = Task.order('created_at DESC') end def create @task = Task.new(task_params) if @task.save render json: @task, status: :created else render json: @task.errors, status: :unprocessable_entity end end def destroy Task.find(params[:id]).destroy! end def update Task.find(params[:id]).toggle!(:is_done) end private def task_params params.require(:task).permit(:name, :is_done) end end endAPIを返す時は、htmlではなくJSONで返してあげたいので以下を追加します。
自分は実際にWEB業界に入るまでJSONに馴染みがなかったのですが、以下の形で書きます。/app/views/api/tasks/index.json.jbuilderjson.set! :tasks do json.array! @tasks do |task| json.extract! task, :id, :name, :is_done, :created_at, :updated_at end endAPIの動作確認
DBにデータを入れて確認してみましょう。
コンソールを立ち上げます。rails cTaskモデルにデータを追加してみましょう。
Task.create(name: 'テスト用タスク')もう一度サーバー立ち上げ
rails s以下のアドレスで追加したデータがJSONでデータが返ればOK。
http://localhost:3000/api/tasks.json
Vue.jsでフロント作成
コンポーネント
コンポーネントとは、名前付きの再利用可能な Vue インスタンスです。
再利用出来そうなパーツごとにコンポーネントを区切って実装するのが、どうやら重要らしい。
今回、最小限の構成でアプリを構成するためコンポーネントについては省こうか迷ったのですが、重要なので組み込みます。わかりやすいイメージで言うと、ヘッダー、フッター、サイドナビ等は色んなページで再利用するのでコンポーネントを分けて実装するといった感じでしょうか。
今回もヘッダーとToDoリストを表示するボディ部分でコンポーネントを分けて実装したいと思います。では、まず以下のようにToDoリスト表示部分を作って下さい。
/app/views/home/index.erb<div id="app"> <navbar></navbar> </div> <%= javascript_pack_tag 'todo' %> # todoに変更する事に注意はヘッダーのコンポーネントを表示します。
<%= javascript_pack_tag 'todo' %>でapp/javascript/packs配下のtodo.jsファイルを読み込みます。ヘッダーの作成
まずはヘッダー部分のコンポーネントを用意します。
/app/javascript/packs/components/header.vue<template> <h1>ToDoリスト</h1> </template>次に
/app/views/home/index.erb
から呼び出されているapp/javascript/packs/todo.js
にコンポーネントの設定をしていきます。
以下のように書くとapp/views/home/index.erb
の<navbar></navbar>
に/app/javascript/packs/components/header.vue
をマウントして表示してくれるようです。/app/javascript/packs/todo.jsimport Vue from 'vue/dist/vue.esm.js' import Header from './components/header.vue' var app = new Vue({ el: '#app', components: { 'navbar': Header } });Vue.jsの読み込み設定
webpackはVue.jsの読み込み方がわからないので以下を実行します(重要)
/config/loaders/stylus.jsmodule.exports = { test: /\.styl$/, use: [ 'style-loader', 'css-loader', 'stylus-loader' ] }/config/webpack/environment.jsconst { environment } = require('@rails/webpacker') const { VueLoaderPlugin } = require('vue-loader') const vue = require('./loaders/vue') const stylus = require('../loaders/stylus') // 作ったstylusをrequireする environment.plugins.prepend('VueLoaderPlugin', new VueLoaderPlugin()) environment.loaders.prepend('vue', vue) module.exports = environment environment.loaders.prepend('stylus', stylus) // 作ったstylusをロードwebpackを再読み込みしてからサーバーを再起動しましょう(重要)
bin/webpackrails srails sしてヘッダーが表示されて入ればOKです
ToDoリストを表示するボディ部分
axiosというライブラリを使って、フロントエンドからHTTPリクエストをします。
以下のコマンドでyarnでaxiosを追加して下さい。yarn add axiosToDoアプリのメイン部分の実装です。解説は後ほど詳しく説明します。
/app/javascript/packs/components/index.vue<template> <div> <div> <input v-model="newTask" placeholder="to doを追加して下さい"> <div v-on:click="createTask"> <i>追加</i> </div> </div> <ul> <li v-for="(task, index) in tasks"> <input type="checkbox" v-model="task.is_done" v-on:click="update(task.id, index)"> <span v-bind:class="{done: task.is_done}">{{ task.name }}</span> <button v-on:click="deleteTask(task.id, index)">削除</button> </li> </ul> </div> </template> <script> import axios from 'axios'; export default { data: function () { return { tasks: [], newTask: '' } }, mounted: function () { this.fetchTasks(); }, methods: { fetchTasks: function () { axios.get('/api/tasks').then((response) => { for(let i = 0; i < response.data.tasks.length; i++) { this.tasks.push(response.data.tasks[i]); } }, (error) => { console.log(error, response); }); }, createTask: function () { if(this.newTask == '') return; axios.post('/api/tasks', { task: { name: this.newTask } }).then((response) => { this.tasks.unshift(response.data); this.newTask = ''; }, (error) => { console.log(error, response); }); }, deleteTask: function (task_id, index) { axios.delete('/api/tasks/' + task_id).then((response) => { this.tasks.splice(index, 1); }, (error) => { console.log(error, response); }); }, update: function (task_id) { axios.put('/api/tasks/' + task_id).then((response) => { }, (error) => { console.log(error); }); } } } </script>作ったindex.vueを読み込んであげましょう。
/app/javascript/packs/todo.jsimport Vue from 'vue/dist/vue.esm.js' import Header from './components/header.vue' import Index from './components/index.vue' // 追加 var app = new Vue({ el: '#app', components: { 'navbar': Header, 'contents' : Index // 追加 } });ヘッダーのしたにindex.vueを表示するため、
<navbar></navbar>
の下に<contents></contents>
を追加します。/app/views/home/index.erb<div id="app"> <navbar></navbar> <contents></contents> </div> <%= javascript_pack_tag 'todo' %>チェックボックスが押されたら取り消し線を表示するためcss追加。
app/assets/stylesheets/home.scss#app li > span.done { text-decoration: line-through; }
rails s
して動くか確認してみて下さい。
実際にToDoリストを追加してみましょう。ToDoアプリのコード解説メモ
学習し始めだと、Vue.jsのどの行が何をやっているのかわからない事があったのでメモ付きのコードをのせます。
まずはtemplate
<template> <div> <div> <input v-model="newTask" placeholder="to doを追加して下さい"> # 追加ボタンを押すとcreateTaskを実行する <div v-on:click="createTask"> <i>追加</i> </div> </div> <ul> # fetchしたタスク一覧(tasks)から一つずつtaskとindexを取り出す処理 <li v-for="(task, index) in tasks"> # チェックボックスが押されたらv-modelのis_doneを変更して取り消し線を引く # updateでAPI側のデータも更新 <input type="checkbox" v-model="task.is_done" v-on:click="update(task.id, index)"> # タスクの表示。v-bind:classでis_doneを参照して取り消し線が引かれるかどうか判定 <span v-bind:class="{done: task.is_done}">{{ task.name }}</span> # 削除ボタンを押すとdeleteTaskを実行 <button v-on:click="deleteTask(task.id, index)">削除</button> </li> </ul> </div> </template><script> import axios from 'axios'; export default { data: function () { return { tasks: [], newTask: '' } }, mounted: function () { this.fetchTasks(); }, methods: { // APIからタスク一覧を取得 fetchTasks: function () { axios.get('/api/tasks').then((response) => { for(let i = 0; i < response.data.tasks.length; i++) { this.tasks.push(response.data.tasks[i]); } }, (error) => { console.log(error, response); }); }, // 新しいタスク作成 createTask: function () { // テキストボックスが空の場合はreturnして終了 if(this.newTask == '') return; // apiへ追加リクエスト axios.post('/api/tasks', { task: { name: this.newTask } }).then((response) => { // unshiftで現在のtasksの先頭にタスクを追加 this.tasks.unshift(response.data); // 追加したらテキストボックスを空にする this.newTask = ''; }, (error) => { console.log(error, response); }); }, // タスク削除 deleteTask: function (task_id, index) { // apiへ削除リクエスト axios.delete('/api/tasks/' + task_id).then((response) => { this.tasks.splice(index, 1); }, (error) => { console.log(error, response); }); }, // タスク更新。今回はis_doneのみ更新だが、タスク名とか色々更新するようカスタムしても良いと思う update: function (task_id) { // apiへ更新リクエスト axios.put('/api/tasks/' + task_id).then((response) => { }, (error) => { console.log(error); }); } } } </script>まとめ
小さいアプリをとりあえず作ってみると理解度がかなり深まると思うので、今回のようなToDoアプリを作ってみると良いと思います。
かけ足で記事を書いてしまったのですが、これで私みたいな人間を救えるのか...???
今後も私のようにVue.js × Railsでアプリを作りたい人向けに記事を改訂して行きたいです。
- 投稿日:2020-01-13T16:21:11+09:00
JavaScript勉強の記録その17: joinとsplitを使った配列の操作
joinメソッド
joinというメソッドを利用することで、配列の要素を繋げて、1つの文字列として値を返すことができます。
以下の例ではjoinメソッドを利用して日付のようなデータを作成しています。
引数に渡した値で、異なる文字列が返ってきているのがわかるかと思います。index.jsconst d = [2020, 1, 13]; console.log(d.join('/')); //=> 2020/1/13 console.log(d.join('')); //=> 2020113 console.log(d.join()); //=> 2020,1,13splitメソッド
splitというメソッドを利用することで、文字列を分割し、配列を作成することができます。
joinとは反対に、文字列から配列を作るメソッドです。以下の例ではsplitメソッドを利用して文字列を分割し、配列を作っています。
引数に渡した値で文字列を区切り、新しく配列を返しています。index.jsconst t = '20:05:30'; console.log(t.split(':')); //=>["20", "05", "30"]splitメソッドを利用して、ことなる変数または定数に分割代入することもできます。
index.jsconst t = '20:05:30'; const [hour, minute, second] = t.split(':'); console.log(hour) //=> "20"
- 投稿日:2020-01-13T15:45:58+09:00
【three.js】Lineオブジェクトを活用して線がクールなWebページをつくる
See the Pen Draw Lines by three.js by MasatomoFukuda (@chitomo12) on CodePen.
Three.jsには様々な立体物を作成するジオメトリがありますが、単なる線を引くLineオブジェクトのような奥行きのない要素はめったに使われていない印象があります。
本記事ではそんなLineオブジェクトを使った線の引き方と、Lineオブジェクトを活用したWebページ例を紹介します。
1. Lineオブジェクトの使い方
Line( geometry: Geometry, material: Material );Lineオブジェクトに表面素材を指定する
Material
と、三次元ベクトルの頂点座標を与えたGeometry
を渡すことで空間内に線を引くことができます。lines.html(...省略...) // マテリアルを設定 const line_material=new THREE.LineBasicMaterial({ color:0xffffff }); // ジオメトリを作成 var line_geometry=new THREE.Geometry(); // ジオメトリに頂点座標を追加 line_geometry.vertices.push( new THREE.Vector3(-100,0,0), new THREE.Vector3(0,100,0), new THREE.Vector3(100,0,0), ); var newline=new THREE.Line(line_geometry, line_material); scene.add(newline); tick(); function tick() { // カメラを回転させる rot+=0.5; const radian=Math.PI/180*rot; camera.position.x=500*Math.sin(radian); camera.position.z=500*Math.cos(radian); camera.lookAt(0,0,0); // レンダリング renderer.render(scene, camera); requestAnimationFrame(tick); } (...省略...)実行例
See the Pen
Draw Lines 01 by three.js by MasatomoFukuda (@chitomo12)
on CodePen.
より細かい情報は公式リファレンスをご参照に。
https://threejs.org/docs/#api/en/objects/Line2. Lineを使ったWebページをつくる
では早速、Lineを活用した簡単なWebページを作っていきます。
まず、ただの線で印象深いグラフィックを作るにはどうすれば良いかを考えます。
兎にも角にもアイデア出しです。ざっと思いついたのが次の3つの案です。
- 一本の線を動かしてみる
- 複数の線を配置して幾何形体を作る
- ランダムに線を配置して複雑な形態を作る
今回は「3. ランダムに線を配置して複雑な形態を作る」のアイデアを採用しました。
まずは、先程作成した線のパラメータを乱数に置き換え、複数描画してみます。
線の描画部分を以下の通りに書き換えてください。lines.html// 線のグループを作る var lines=new THREE.Group(); const line_material=new THREE.LineBasicMaterial({ color:0xffffff }); // for文でランダムな線(newline)を複数作る for(var i=0;i<10;i++){ var line_geometry=new THREE.Geometry(); line_geometry.vertices.push( new THREE.Vector3(-Math.random()*100, 0, 0), new THREE.Vector3(0, Math.random()*100, 0), new THREE.Vector3(Math.random()*100, 0, 0), ); var newline=new THREE.Line(line_geometry,line_material); // newlineをグループに追加 lines.add(newline); } // linesグループをシーンに追加 scene.add(lines);大きな変更点として、複数の線をまとめるためのグループを作成し、線の頂点を乱数に変更しました。
実行すると下図のようになります。
すでに良い感じに見えますが、少し物足りなさは感じるのでどうにかしたい。ここはシンプルに情報量を増やしたいと思います。
具体的には頂点の数を増やし、さらにz座標のパラメータもいじることで奥行きもつくっていきます。さきほどのfor文内に、頂点を追加するための行を追加します。
lines.htmlfor(var i=0;i<10;i++){ var line_geometry=new THREE.Geometry(); line_geometry.vertices.push( new THREE.Vector3(-Math.random()*300,0,-Math.random()*300), new THREE.Vector3(0,Math.random()*300,-Math.random()*300), new THREE.Vector3(Math.random()*300,0,-Math.random()*300), new THREE.Vector3(Math.random()*300,0,-Math.random()*300), new THREE.Vector3(Math.random()*300,0,-Math.random()*300) ); var newline=new THREE.Line(line_geometry,line_material); lines.add(newline); }実行すると次のようになります
最初はただの線でしたが、ここまで来たら複雑な立体物を使いこなしている感が出てきたかと思います!さて、今回作ろうとしているのはWebページのサンプルです。
今のままだと3D空間内を大きく移動するような迫力ある視覚経験はできると思いますが、ゲームではなくWebページなのでこれをもう少し落ち着いたUXにしたい。
カメラが大きく空間を動くのではなく、カメラは動かないままオブジェクトだけが回る、そういう空間を作ろうと思います。
lineオブジェクトの集合の中心が座標 (0,0,0) に来るようにし、ページ全体に線が均等に現れるように調整を加えます。
for文を次のように書き換えてください。lines.htmlfor(let i=0;i<20;i++){ var line_geometry=new THREE.Geometry(); line_geometry.vertices.push( new THREE.Vector3(0,0,0), new THREE.Vector3(Math.random()*400-200,Math.random()*400-200,Math.random()*400-200), new THREE.Vector3(Math.random()*400-200,Math.random()*400-200,Math.random()*400-200), new THREE.Vector3(Math.random()*400-200,Math.random()*400-200,Math.random()*400-200), new THREE.Vector3(Math.random()*800-400,Math.random()*400-200,Math.random()*800-400), new THREE.Vector3(Math.random()*800-400,Math.random()*400-200,Math.random()*800-400), new THREE.Vector3(Math.random()*400-200,Math.random()*4000-200,Math.random()*400-200) ); var newline=new THREE.Line(line_geometry,line_material); lines.add(newline); }そしてこれを実行すると下図のようになります。
※画質が荒くてすみません? 実際の見た目はもっと鮮明です。これでもう、一つの映像として十分強度が出てきたのではないでしょうか?
仕上げとして、WebページのCSSやナビゲーション要素などを配置し、マウス位置に応じたインタラクティビティも加えます。
追加要素を列挙すると以下の通り。・背景を水色に変更。
・回転するオブジェクトが画面右(カメラに対して少し右)に来るよう、時間経過でx-z座標を移動するように設定。
・DIVタグでテキストを作成し、CSSでpositionプロパティにabsoluteを入れて良い感じの位置に配置。
・マウス位置に合わせて回転の速度を増減する。こうして出来上がったものが、冒頭でも紹介した以下のページになります。
See the Pen Draw Lines by three.js by MasatomoFukuda (@chitomo12) on CodePen.
締め
lineオブジェクトを使った事例が意外と少なかったので色々調べてみたのがこの記事を書いたきっかけだったのですが、マテリアルやシェーダーなど複雑な要素を用いることなく、ただの線を引くだけでそれなりに見映えするWebページを1つ作ることができたのは個人的にも意外な発見でした。
この記事がthree.jsをもっと楽しむきっかけになれば幸いです。
参考
公式リファレンス:https://threejs.org/docs/index.html#manual/en/introduction/Drawing-lines
Three.js入門サイト - ICS MEDIA:https://ics.media/tutorial-three/
- 投稿日:2020-01-13T15:22:23+09:00
Unity+Googleスプレッドシート+GASでサーバーレスのデータベースシステムを実現する?
前書き
unity1weekをきっかけに、Unity+Googleスプレッドシート+GASで簡易ランキング機能を作ってみました。この仕組みで、ランキングだけではなく、任意のデータをアップロード・ダウンロードできれば、一応汎用的なデータベースとして使えるのではないかと思いました。例えばゲームのマスタデータをGoogleスプレッドシートに保存することで、アプリのバージョン更新なしでゲームの調整ができてまうとか、ゲームの最新バージョンをGoogleスプレッドシートに書き込んで、アプリ起動時にそれを取得して古ければ強制アップデートポップアップを出すとか、さらにチャットやユーザー情報の管理もGoogleスプレッドシートでやりとりするなど、ユースケースがどんどん湧いてきます。
ということで、実装してみました。使い方
プロジェクトはUmbrella(トトロなので)という名でGithubに公開しました。最新パッケージのダウンロードはこちら。ちなみに、Umbrellaには単純なデータ通信管理システムDatabase以外に、簡易ランキングシステムRankingも含まれています。興味ある方はぜひ合わせていじってみてください。
GAS側
- 新しいGoogleスプレッドシートを作成する。
- メニューのツールからスクリプト エディタをクリックする。
- Assets/Umbrella/Database/Database.gsの内容をコード.gsにコピーする。
- メニューのファイル > 保存でプロジェクトに名前をつけて保存する(保存にちょっと2、3秒ぐらい掛かるかも)。
- メニューの公開 > ウェブアプリケーションとして導入...をクリックする。
- ウェブアプリケーションとして導入のポップアップにて、次のユーザーとしてアプリケーションを実行に自分のアカウントを、アプリケーションにアクセスできるユーザーに全員(匿名ユーザーを含む)を設定する。
- 導入をクリックして、現在のウェブアプリケーションのURLの下に書いてあるURLをコピーする。
- 「認証が必要です」のポップアップが出たら@zk_phiさんの記事を参考にして認証を行ってください。
Unity側
- Assets/Umbrella/Database/DatabaseManager.prefabをデータをやりとりしたいシーンのヒエラルキーにドラッグ&ドロップする。
- プレハブインスタンスのインスペクターから、App URLのフィールドに先ほどコピーしたGASのウェブアプリケーションURLをペーストする。
- Default Sheetフィールドに使いたいGoogleスプレッドシートのデフォルトシート名を入力する。
- スクリプト内で、データを送信したい場合は
DatabaseManager.Instance.SendDataAsync(data, handleResponseCallback, sheetName)
を呼び、データを取得したい場合はDatabaseManager.Instance.GetDataAsync(key, handleResponseCallback, sheetName)
を呼ぶ。また、メソッドの前にyield return
を付ければデータ取得後の処理(handleResponseCallback)の実行完了まで待つことができる。- 具体的な使い方はサンプルシーンとスクリプトを参照してください。
デモ
他のクライアントとしてデータを送信します。Umbrellaではクライアントを識別するユニークIDをUnity側で生成してPlayerPrefsに保存しているため、PlayerPrefsをクリアしないまま送信すると既存のデータを上書きすることになります。
実装の抜粋
GAS側の処理
GASはUnityから送ってきたデータに基づき、スプレッドシートの中身を更新します。
function doPost(e) { var request = e.parameter; var method = request[CONST.Method]; if(method == CONST.SaveData){ return saveData(request); }else if(method == CONST.GetData) { return getData(request); } return ContentService.createTextOutput("Error: Invalid method"); }
saveData
とgetData
の実装詳細はここでは省略しますが、GASのAPIコールはコスト高いため、処理速度を高めにはできる限りAPIコールの回数を減らす必要があります。例えばセルデータの取得で、各セルでsheet.getRange().getValue()
の代わりに、予めvar data = sheet.getDataRange().getValues()
で指定範囲のセルデータを一括で配列に格納し、後で配列から値を取るなどの策が考えられます。また、データの書き込みがある場合、排他処理(ロック)を入れる必要がありますが、一行のデータをまとめて一括でappendRow()を使えばセル単位でロックをかける手間をなくすテクニックもあります。appendRow()は不可分操作(Atomic Operation)なので排他処理が要らないからです。UnityからGASへの送信
簡易のデータベース機能なので、特に通信の仕様とかは決めなく(暗号化??)、完全にJSON形式で送信しています。JSON解析は軽量のMiniJsonを導入しています。
public CustomYieldInstruction SendDataAsync(MonoBehaviour context, string methodName, string sheetName, Dictionary<string, object> data, Action<object> handleResponse = null) { var strData = Json.Serialize(data); var formData = new List<IMultipartFormSection>(); formData.Add(new MultipartFormDataSection("method", methodName)); formData.Add(new MultipartFormDataSection("sheet", sheetName)); formData.Add(new MultipartFormDataSection("data", strData)); bool complete = false; context.StartCoroutine(CT_SendData(formData, status => complete = status, handleResponse)); return new WaitUntil(() => complete); }
WWWForm
がLegacyになったので、IMultipartFormSection
でフォームデータを作成しています。フォームデータにGAS側で呼び出したいメソッド名、使いたいシート名とデータ内容を入れています。また、外部でyield return
をつけて待たせられるように、返り値のタイプをCustomYieldInstruction
にしてcomplete
がtrueになるまで処理を止めることを可能にしています。
UnityWebRequest
のPost
メソッドでフォームデータを送信しています。private IEnumerator CT_SendData(List<IMultipartFormSection> formData, Action<bool> updateStatus, Action<object> handleResponse = null) { updateStatus(false); var www = UnityWebRequest.Post(_appURL, formData); Debug.Log("<color=blue>[GSSDataService]</color> Start sending data to Google Sheets."); yield return www.SendWebRequest(); if (www.isNetworkError || www.isHttpError) { Debug.LogError($"<color=blue>[GSSDataService]</color> Sending data to Google Sheets failed. Error: {www.error}"); } else { Debug.Log("<color=blue>[GSSDataService]</color> Sending data to Google Sheets completed"); try { var response = Json.Deserialize(www.downloadHandler.text); string message = response as string; if (message != null && message.Contains("Error")) Debug.LogError($"<color=blue>[GSSDataService]</color> Getting data from Google Sheets failed. {message}"); else handleResponse?.Invoke(response); } catch (InvalidCastException e) { Debug.LogError($"<color=blue>[GSSDataService]</color> Parsing result from Google Sheets failed. Error: {e.Message}"); } } updateStatus(true); }返ってきた結果にError文字列(GAS側で入れている)が含まれたらエラーログを書き出し、なければ結果を処理する
handleResponse
メソッドを呼び出します。後書き
Unity+Googleスプレッドシート+GASでサーバーレスの簡易データベース機能を作ってみました。当然いくつか問題もあります。
- 完全JSON形式でデータのやりとりをしているため、複雑のデータ構造に対応できない(自分でシリアライザとデシリアライザを書くなどの工夫が要る)。
- 安全性一切考えていない(Google神がいい感じにしてくれるはず)。
- 負荷検証や処理速度を計測・比較していないので不明(使った肌感だと耐えられレベルの遅延)。
- そもそもGoogleのサービスに制限がある。URL Fetch callsだと、無料のGmailアカウントで1日2万回までしか呼べないので、大規模や非常に頻繁な通信に向いていない。
しかしながら個人プロジェクトレベルのものとしては十分機能できるのではないかと思います。何より、Googleスプレッドシートならではの機能が使えて、直接データを一目瞭然で見たり、気軽にデータを修正したりすることができる点から、普通のSQLデータベースよりも便利かもしれません?(?)
おまけ
Umbrellaにランキング機能もついているので、それの使い方も紹介します。
1. Assets/Umbrella/Ranking/RankingManager.prefabをランキングを表示したいシーンのヒエラルキーに置いておく。
2. Assets/Umbrella/Ranking/RankingSettings.assetのインスペクターから、App URLフィールドにGASのウェブアプリケーションURLをコピーする。
3. Ranking Request Settingsフィールドに、ランキングの種類ごとで配列に要素を入れていく。Ranking Nameはランキングの名前で、Ranking Numberは上位何位まで取得するかを指定し、Order Byは昇順(ASC)か降順(DESC)を選べる。
4. スクリプト内でRankingManager.Instance.SendScoreAsync(playerName, score, handleResponseCallback, rankingRequestIndex)
を呼んでスコアを送信し、RankingManager.Instance.GetRankingListAsync(handleResponseCallback, rankingRequestIndex)
でランキングリストを取得できる。同様に、メソッドの前にyield return
を付ければデータ取得後の処理(handleResponseCallback)の実行完了まで待つことができる。
5. 具体的な使い方はサンプルシーンとスクリプトを参照してください。参考
- 投稿日:2020-01-13T15:00:35+09:00
JavaScript勉強の記録その16: 文字列の操作
文字列の長さを取得する
JavaScriptには文字列を操作する為の方法がいくつか用意されてありますので、メモとして書き残します。
index.jsconst str = 'hello'; console.log(str.length); //=>5文字へのアクセス
文字へのアクセスの方法は、文字列[インデックス番号]とすることで可能です。
以下の例ではhelloという文字列の1番目と2番目にアクセスし、コンソールに表示しています。index.jsconst str = 'hello'; console.log(str[1]); //=>e console.log(str[2]); //=>l console.log('hello'[1]); //=>esubstring()を利用して、文字列の一部を切り出す
substring()という文字列を操作するメソッドを使えば、文字列の一部を切り出すことができます。
substring(切り出しを開始したいインデックス番号、切り出しを終了したいインデックス番号)というように引数を取れば利用することができます。具体的な使い方としては以下です。
substringメソッドの第一引数は2で第二引数を4とすることで、「ll」のみを取ることができます。index.jsconst str = 'hello'; console.log(str.substring(2, 4)); //=>ll
- 投稿日:2020-01-13T13:18:04+09:00
複数のファイルをアップロードしてExpressサーバーで保存する
概要
クライアントアプリからExpressサーバーへの複数ファイルのアップロード時にハマったので、
方法を記載します。環境
開発環境はMacでNodebrewを使っています。
$ nodebrew nodebrew 1.0.1 $ node -v v12.14.1単一ファイルのアップロード
まず単一ファイルのアップロード方法を記載します。
Expressサーバー立ち上げ
express-generatorでExpressサーバーを作成します。
$ npm install -g express-generatorexpress-generatorをインストールしてexpressコマンドが使えるようになったので
アプリを作成します。$ express --view=ejs express-app create : express-app/ create : express-app/public/ create : express-app/public/javascripts/ create : express-app/public/images/ create : express-app/public/stylesheets/ create : express-app/public/stylesheets/style.css create : express-app/routes/ create : express-app/routes/index.js create : express-app/routes/users.js create : express-app/views/ create : express-app/views/error.ejs create : express-app/views/index.ejs create : express-app/app.js create : express-app/package.json create : express-app/bin/ create : express-app/bin/www change directory: $ cd express-app install dependencies: $ npm install run the app: $ DEBUG=express-app:* npm start上の手順の通りにサーバーを立ち上げます。
$ cd express-app/ $ npm install $ npm start以下のURLにアクセスしてサーバーに接続できることを確認します。
http://localhost:3000アップロードAPI作成
サーバーを立ち上げることができたので、一度停止してアップロードAPIを作成します。
app.jsを以下のように修正します。app.jsvar createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); // 追加 var uploadRouter = require('./routes/upload'); var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); app.use('/users', usersRouter); // 追加 app.use('/upload', uploadRouter);FormDataを処理するためにmulterを追加します。
https://github.com/expressjs/multer$ npm install multerroutesフォルダ内にupload.jsを作成します。
destで指定したフォルダ内にアップロードされたファイルが保存されます。upload.jsvar express = require('express'); var multer = require('multer'); var upload = multer({ dest: 'uploads/' }); var router = express.Router(); router.post('/', upload.single('file'), function(req, res, next) { console.log(req.file); console.log(req.body); res.send('upload success'); }); module.exports = router;これでAPIが作成できたのでnpm startでサーバーを立ち上げておきます。
アップロードフォーム作成
クライアントアプリ側に以下のようなファイルを作成してアップロードフォームを用意します。
Ajaxを使ってファイルと一緒に適当なパラメータも送信しています。upload.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>Uploader</title> <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> </head> <body> <div> <input type="file" id="upload"> <input type="button" id="uploadButton" value="送信"> </div> <script> $(function(){ $('#uploadButton').click(function() { const files = $('#upload')[0].files; const formData = new FormData(); formData.append('file', files[0]); formData.append('hoge', 123); $.ajax({ url: 'http://localhost:3000/upload', method: 'post', data: formData, processData: false, contentType: false }).done(function(res){ console.log(res); }).fail(function(err) { console.log(err); }) }) }); </script> </body> </html>ファイルアップロード
Expressサーバーを立ち上げている状態で上で作成したupload.htmlをブラウザで開いて、
ファイルを選択後に送信ボタンを押下します。Expressのコンソールに以下のような値が出力されています。
req.fileにアップロードしたファイルの情報、req.bodyにパラメータが格納されています。{ fieldname: 'file', originalname: 'upload_file.txt', encoding: '7bit', mimetype: 'text/plain', destination: 'uploads/', filename: '12dee747383a844dd7d1888578cf720e', path: 'uploads/12dee747383a844dd7d1888578cf720e', size: 247379 } [Object: null prototype] { hoge: '123' } POST /upload 200 5.874 ms - 14multerのdestにuploadsを指定したので、Expressアプリのルートディレクトリにuploadsフォルダが作成されて
その中にアップロードしたファイルが保存されています。もし送信ボタン押下時にクロスドメインのエラーが発生する場合は、
以下のようにExpressサーバーにCORSの許可設定を行なってください。CORS対応
ファイルアップロード時にクロスドメインエラーが発生した場合はExpressにCORSの許可を設定する必要があります。
方法はいくつかありますが、今回はcorsモジュールを使用します。$ npm install cors上のコマンドを実行後にapp.jsに以下を追記してください。
今回は全リクエストを許可としていますが、本番運用などする際は適切に設定を行なってください。app.jsvar createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); // 追加 var cors = require('cors'); var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); var uploadRouter = require('./routes/upload'); var app = express(); // 追加 app.use(cors());この状態でサーバーを立ち上げてファイルアップロードを行うとクロスドメインエラーが発生しないようになっています。
複数ファイルのアップロード
前置きが長くなりましたが、次に複数ファイルのアップロードを行います。
アップロードAPI作成
まずExpressに複数ファイルのアップロードAPIを作成します。
比較用に単一ファイル用のAPIも残しています。upload.jsvar express = require('express'); var multer = require('multer'); var upload = multer({ dest: 'uploads/' }); var router = express.Router(); // 単一ファイルアップロード router.post('/', upload.single('file'), function(req, res, next) { console.log(req.file); console.log(req.body); res.send('upload success'); }); // 追加 // 複数ファイルアップロード router.post('/multiple', upload.array('files'), function(req, res, next) { console.log(req.files); console.log(req.body); res.send('multiple upload success'); }); module.exports = router;アップロードフォーム作成
クライアント側にも複数ファイルのアップロードフォームを追加します。
ポイントは複数ファイルをFormDataに追加する際に'files'を指定する部分です。upload.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>Uploader</title> <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> </head> <body> <!-- 単一ファイルアップロードフォーム --> <div> <input type="file" id="upload"> <input type="button" id="uploadButton" value="送信"> </div> <!-- 追加 --> <!-- 複数ファイルアップロードフォーム --> <div> <input type="file" id="multipleUpload" multiple> <input type="button" id="multipleUploadButton" value="送信"> </div> <script> $(function(){ // 単一ファイルアップロード $('#uploadButton').click(function() { const files = $('#upload')[0].files; const formData = new FormData(); formData.append('file', files[0]); formData.append('hoge', 123); $.ajax({ url: 'http://localhost:3000/upload', method: 'post', data: formData, processData: false, contentType: false }).done(function(res){ console.log(res); }).fail(function(err) { console.log(err); }) }) // 追加 // 複数ファイルアップロード $('#multipleUploadButton').click(function() { const files = $('#multipleUpload')[0].files; const formData = new FormData(); for (let i = 0; i < files.length; i++) { formData.append('files', files[i]); } formData.append('hoge', 123); $.ajax({ url: 'http://localhost:3000/upload/multiple', method: 'post', data: formData, processData: false, contentType: false }).done(function(res){ console.log(res); }).fail(function(err) { console.log(err); }) }) }); </script> </body> </html>ファイルアップロード
Expressサーバーを立ち上げている状態で、先ほどと同じようにブラウザから複数ファイルを選択してアップロードを行います。
Expressのコンソールでファイルの情報が出力されて、uploadsフォルダにファイルが保存されていることが確認できます。[ { fieldname: 'files', originalname: 'upload_file 2.txt', encoding: '7bit', mimetype: 'text/plain', destination: 'uploads/', filename: '2c8436e8d77723dfaf7a75e38fe1785c', path: 'uploads/2c8436e8d77723dfaf7a75e38fe1785c', size: 247379 }, { fieldname: 'files', originalname: 'upload_file.txt', encoding: '7bit', mimetype: 'text/plain', destination: 'uploads/', filename: '5ed6ed7671d65f269dfaa2c456b2b95b', path: 'uploads/5ed6ed7671d65f269dfaa2c456b2b95b', size: 247379 } ] [Object: null prototype] { hoge: '123' } POST /upload/multiple 200 19.628 ms - 23まとめ
単一ファイルのアップロードは割と簡単に実装できましたが、
複数ファイルのアップロードでのクライアントからのFormDataへの追加と
サーバーでの保存がなかなかうまくいかずハマってしまいました。Formタグを使えばもう少し簡単に実装できたかもしれませんが、
今回は使用せずに実装したかったためこのような方法になりました。
- 投稿日:2020-01-13T12:47:39+09:00
Chromebook向けにツールを作る
はじめに
Chromebookって便利ですよね?ただ痒いところに手が届かないことも多々あり、解決すべくツールを作ろうと思い立ちました。
どのアーキテクチャ、フレームワークが向いてる?
Linuxが動いたりAndroidアプリが使えたり、Chrome OSネイティブアプリの存在感が皆無だったりしたのでざっくり纏めてみました。
Chrome App
- JavaScript向けのChrome APIsを使って、manifestを定義すればChromeアプリが作れる。APIの使い方はsampleを見るのがよい。
- Windows、Mac、Linux向けが途絶えたりしてChrome OS向けだけ残った状況。
- Native Clientという低レイヤー用のSDKもあるようだ。
Chrome Extension
- Chrome Browser内で閉じられる場合はこれ。
Progressive Web App
- Chrome AppではなくPWAがメインストリームなのでしょうか。ファイル操作とかのAPIが使えないのでツール開発には向かなそう。
Android App
- Chrome OSをサポートしたAndroidアプリもChrome OSアプリというようだが、さくっと作れるものではない。
Linux上で動かす
- ツールを動かすためにCrostiniを有効にはしたくない。立ち上がるの遅いし、メモリ食うし。
Flutterアプリ
- Flutter SDKはChromebookもサポートしてるけど、ツールを作るのにここまでしたくない。
結論
現状(バージョン: 79)でtabletで動かすケースではChrome Appがベターですかね。
ファイル操作だけ出来ればいいならNative File System使うのがいいかも。ハローワールド
- Chrome browserを開いて、拡張機能のデベロッパーモードを有効にする。
- samplesから参考になりそうなのを見つけて、「パッケージ化されていない拡張機能を読み込む」で開く。
- アプリのアイコンがメニュー登録されているので、実行する。
- デバッグは拡張機能の一覧からアプリの「ビューを検証」のバックグラウンドページ、index.htmlをクリックしてChrome DevToolsを開く。
- 修正したら拡張機能の一覧からアプリをリロードしてデバッグする。
- 投稿日:2020-01-13T12:28:49+09:00
【JavaScript】2種類の恐竜画像を任意の数だけ表示させてみる。
はじめに
会社で使っている言語以外にも、何か身につけたい。
仕事では全く違う言語を使っていることもあって、楽しくないと新しい知識が吸収できなくなっている。
作って楽しい気分になるプログラムを考えてみた。今回登場のメンバーはステゴディアスとインドミナスレックス。
ボタンを押すと、この二匹のどれかが、任意の数だけ表示されるプログラムを作ってみた。1.今回のポイント
- 任意の数だけイメージタグを作りたい。
- idはわかりやすいように数字を入れたい。
- 乱数(ここで表示する画像数を決定する)
- インドミナスの画像が沢山表示されたら、攻めの姿勢で、ステゴディアスが表示されたならソースの実行確認などを念入りに行って守りを固めるとよいでしょう
2.実際のソース
dino_print_random.html<!DOCTYPE html> 任意の数だけ恐竜を表示<br> <form name="test"> <input type="button" value="dinosor_print!!" onClick="OnClickmake()"/> </form> <div id = "dino"></div> <body> <script type="text/javascript"> function OnClickmake(){ var min=2; var max=6; //表示する画像の個数 var num_dino=Math.floor( Math.random() * (max + 1 - min) ) + min; //表示する画像の種類 var type0=Math.floor( Math.random() * (max + 1 - min) ) + min; appendImage(num_dino,type0); } function appendImage(num1,d_type) { if (!document.createElement || !document.getElementById) return; alert(num1); var str_dino=""; //1つのイメージタグに設定する内容 var c_dino=""; //イメージのid var str_id=""; //4以下ならばステゴディアス if(d_type<4){ img_src="./stedia.jpg"; //4以上ならインドミナスレックス }else{ img_src="./indominas.jpg"; } for(var i=0;i<num1;i++){ //イメージのid ダブルコーテーションはエスケープ(\)が必要 str_id="\""+"img"+i+"\""; c_dino="<img id="+str_id+ " src = "+img_src+" width=\"360\" height=\"240\"><br><br>"; str_dino=str_dino+c_dino; } document.getElementById("dino").innerHTML = str_dino; } </script> </body> </html>実行結果
上記の2つの画像にそれぞれstedia.jpg,indominas.jpg
と名前を付けて、ソースと同じフォルダにおいてブラウザで実行すると
下記のような画像が表示されます。
- 投稿日:2020-01-13T12:27:42+09:00
JavaScript勉強の記録その15: 配列にオブジェクトを格納するデータ構造
- 投稿日:2020-01-13T12:14:13+09:00
JavaScript勉強の記録その14: Object.keys(オブジェクト名)を利用してオブジェクトのプロパティーを操作
Objet.keys(オブジェクト名)を利用して、オブジェクトのプロパティーを操作
オブジェクトのプロパティーの値を全て取り出したい場合は、forEachは使えません。なぜならforEachは配列に対するメソッドだからです。
代わりにObject.keys(オブジェクト名)というメソッドを使い、オブジェクトのキーを取得し、取得したキーを利用してオブジェクトの値を取り出す方法があります。
まず、以下の例ではObject.keys()の基本的な動きを確認できます。
index.jsconst point = { x: 100, y: 180, }; const keys = Object.keys(point); console.log(keys); //=>["x", "y"] keys.forEach(key => console.log(`Key: ${key}`)); //=>Key: x //=>Key: yObject.keys()を使うことで、キーを取得して配列に格納しているのがわかるかと思います。
この仕組みを利用してループ処理をしてあげれば、オブジェクトの各プロパティーへアクセスすることができます。index.jsconst point = { x: 100, y: 180, }; const keys = Object.keys(point); console.log(keys); //=>["x", "y"] keys.forEach(key => console.log(`Key: ${key} Value: ${point[key]}`)); //=>Key: x Value: 100 //=>Key: y Value: 180