- 投稿日:2020-06-24T23:43:51+09:00
攻撃から学ぶWEBセキュリティ対策(XSS編)
はじめに
WEBエンジニアとして成長するために、セキュリティ対策は避けては通れない道ですよね。
僕も含め「なんとなく知ってる」という方は多いのではないでしょうか。
大切なWEBサイトを守るためにも、WEBエンジニアとしての基礎を固める為にも、しっかりと学んで一緒にレベルアップしていきましょう。
また、本記事の内容は様々な文献をもとに自身で調べ、試したものをまとめています。
至らぬ点や、間違いがありましたら、コメントにてご指摘をお願いします。XSSってなに?
XSS(Cross-site scripting)とは、他人のブラウザーで悪意のあるJavaScriptを実行するコードインジェクション攻撃です。
ターゲットに標的を定めて攻撃するのではなく、ターゲットがアクセスするWEBサイトの脆弱性を悪用して、悪意のあるJavaScriptを実行します。
このように攻撃者が直接攻撃していることは被害者には分からないので
自分のサイトが気がついたら犯罪に加担していた
という最悪の事態も考えられます。
JavaScriptでどんな攻撃ができるか
上記のように悪意のあるコードを実行することで、以下のような攻撃が可能です。
- Cookieなどの機密情報の一部へのアクセス
- XMLHttpRequestなどを利用して任意の情報をHTTPリクエストを任意の宛先に送信
- DOM操作メソッドを使用して、WEBページのHTMLに変更を加える
単体ではあまり効果を発揮しませんが、これらを組み合わせることで強烈な攻撃を行うことが出来ます。
Cookieの盗難
攻撃者は、Webサイトに関連付けられた被害者のCookieにアクセスして、自分のサーバーに送信することで、セッションIDなどの機密情報を取得することが可能です。
キーロギング
キーロギングとはユーザーがキーボードでPCに入力する内容を密かに記録する行為です。
これにより、パスワードやクレジットカード番号などの機密情報を記録して任意の宛先に送信することが可能です。フィッシング
DOM操作を使用して偽のログインフォームをページに挿入し、formのaction属性を任意のサーバーに設定することで機密情報を送信させることが可能です。
これらはあくまで一例で、組み合わせ次第では更に手の込んだ攻撃が出来ます。
XSSのタイプ
XSSには3つのタイプが存在します。
Persistent XSS
- 攻撃者はWebサイトのフォームを使用して、データベースに悪意のあるスクリプトを挿入します。
- 被害者はWebサイトにページをリクエストします。
- Webサイトの応答には、データベースからの悪意のあるコードが含まれ、被害者に送信されます。
- 被害者のブラウザ内で、悪意のあるスクリプトを実行され、被害者の情報が攻撃者のサーバーに送信されます。
Reflected XSS
- 攻撃者は悪意のあるスクリプトを含むURLを作成して被害者に送信します。
- 被害者は、悪意のあるスクリプトを含むURLのページをリクエストします。
- レスポンスにはURLに含まれる悪意のあるスクリプトが含まれています。
- 被害者のブラウザ内で、悪意のあるスクリプトを実行され、被害者の情報が攻撃者のサーバーに送信されます。
DOM-based XSS
- 攻撃者は悪意のあるスクリプトを含むURLを作成して被害者に送信します。
- 被害者は、悪意のあるスクリプトを含むURLのページをリクエストします。
- レスポンスにはURLに含まれる悪意のあるスクリプトが含まれていません。
- 応答された正当なDOMをレンダリングした後に、悪意のあるスクリプトがページに挿入されます。
- 被害者のブラウザ内で、挿入された悪意のあるスクリプトを実行され、被害者の情報が攻撃者のサーバーに送信されます。
Reflected XSS と DOM-based XSSの違い
この2つのタイプはとても似ています。
Reflected XSSは、悪意のあるスクリプトをページに挿入した状態で応答します。
被害者のブラウザが応答を受信すると、悪意のあるスクリプトがページの正当なコンテンツの一部であると想定し、他のスクリプトと同様にページのロード中にそれを自動的に実行しますDOM-based XSSは、悪意のあるスクリプトを含まない正当な状態で応答します。
ページの読み込み中に自動的に実行されるスクリプトはあくまで正当なものです。
ページが読みこまれた後に既存のJavaScriptの脆弱性が原因で悪意のあるスクリプトが含まれたURLが実行されてしまいます。Reflected XSS や Persistent XSS の例では、URLに悪意のあるスクリプトを仕込むことで、攻撃していた為、JavaScriptは必要ありませんでした。
つまりサーバー側のコードに脆弱性がない場合、WebサイトはXSSの対策が出来ていると言えます。
ですが、近年のWebアプリケーションの発達に伴い、サーバーではなくクライアント側のJavaScriptによって生成されるHTMLの量が増えています。
つまり、XSSの脆弱性は、サーバー側のコードだけでなく、クライアント側のJavaScriptコードにも存在する可能性があると言えます。
XSSへの対策について
XSS攻撃は悪意のあるスクリプトを、ユーザーの入力値として解釈されることが原因で起こる、コードインジェクション攻撃です。
このタイプの攻撃を防ぐには、安全な入力処理が必要になります。
僕たちWEB開発者ができる対策を挙げてみましょう。
対策の細かい設定は別に調べてもらった方がいいと思うので簡略的に説明します。エスケープ処理の追加
入力された値をエスケープし、スクリプトではなく文字列としてのみ解釈するようにする。
厳密なバリデーション処理
想定する値以外を入力できないように、可能な限り厳密に入力をフィルタリングする。
適切なレスポンスヘッダーの使用
HTMLまたはJavaScriptを含めることを意図していないHTTP応答でのXSSを防ぐために、
Content-Type
およびX-Content-Type-Options
ヘッダーを使用して、ブラウザーが意図したとおりに応答を解釈できるようにする。エンコード処理の追加
ユーザーが入力したデータがHTTP応答で出力される場合は、出力をエンコードしてアクティブコンテンツとして認識されないようにする。
CSPルールのセット
コンテンツセキュリティポリシー(CSP)ルールの適切なセットを適用すると、ブラウザーがインラインJavaScript、eval()、setTimeout()、または信頼できないURLからのJavaScript などを実行しないようにする。
まとめ
実際に文字にしてみると、分からない事や、不明確なまま理解している事が多かったです。
セキュリティ
という大きな検索で調べるのではなく、XSS
などの具体的な攻撃方法で検索した方が、たくさんの情報がヒットするので、興味のある方は更に調べてみてください。海外エンジニアの記事などはサンプルコードなども掲載しており、とても参考になりました。
次はXSRFについてまとめていきたいと思います。
参考文献
A comprehensive tutorial on cross-site scripting
JavaScript Security Issues and Best Practices
- 投稿日:2020-06-24T23:13:26+09:00
【jQuery】手数料を計算して表示させたい
ユーザーが入力した数値に計算を加えて表示させる際の手順について
簡潔にまとめたのでご紹介します。
(今回は、価格と販売利益として表示します。)1. HTMLで価格コーナーを準備する
sample.haml~省略~ = f.number_field :price, placeholder: "0", class: 'exhibit-form_price' do -#入力欄 %span.mark ¥ .exhibit-form_right .commission-line .commission-line_left 販売手数料 (10%) .commission-line_right ー .profit-line .profit-line_left 販売利益 .profit-line_right ー ~省略~2. Jsの準備をする
フォームの入力値を取得し、任意の計算をした数値を
販売利益の欄に差し込みます。sample.js$('.exhibit-form_price').on('keyup', function(){ var data = $(this).val(); var profit = Math.round(data * 0.9) var fee = (data - profit) $('.commission-line_right').html(fee) $('.commission-line_right').prepend('¥') $('.profit-line_right').html(profit) $('.profit-line_right').prepend('¥') $('#price').val(profit) if(profit == '') { $('.profit-line_right').html(''); $('.commission-line_right').html(''); } })
以上で終了です。
ご覧いただきありがとうございました。
- 投稿日:2020-06-24T23:05:01+09:00
GitHubの草を草色に戻すChrome拡張作った
GitHubのデザインが色々変わって、草が草の色じゃなくなったのが寂しかったので、草色にするChrome拡張を作りました。
効果
before(新デザイン)
after(Chrome拡張を実行)
どうですか?草ですね。落ち着きますね。
入手方法
まだChromeExtensionのストアに申請出していないので(出しても承認に数日かかる)、ローカルでビルドしてください。
リポジトリはこちらです。
ビルド方法
READMEに書きましたが、
cd extension npm install npm buildして、手動で拡張機能をインストールしてください。
実行方法
草のページを開いたら、ツールバーの草アイコン(↓のやつ)をクリックしてください。
本当はページ開いたら自動で実行されるようにしたいですが、未対応です。
実装の解説
草本体
GitHubの草はSVGで実装されています。SVGはHTMLのDOMと同じように、JavaScriptでごりごり変更する事が可能です。
図で示しているのは、1個の草(1日の草?)です。ぱっと見、fillで入っている値が、新しい値になったのかと思いきや、この値は昔のスタイル(草色)のスタイルのままです。
scss(要はcss)で旧スタイルの色から、新スタイルの色に変換するclassを適用する事で、結構無理やり色を変えています。
なので、色を変更する処理としては、
// attributesのfillは昔のcolorがそのまま入っていて、 // 新旧のcolorをmappingしたscssで色が変わっている // なので、styleでcolorを設定してあげればscssより優先されるので元の色に戻る const fill = kusa.getAttribute('fill'); kusa.style.fill = fill;という風に、
fill
の値をstyleに設定してあげれば昔の色になります。
(classよりstyleで直書きの方が強いので)legend
legendはここの見本の事です。class名がlegendってなってたのですが、そういう意味あるんですかね??
で、ここはSVGではなくて、ulとliで実装されています。
styleとして直書きされている方には昔の色がそのまま入っていて、scss(css)で
!important
で色を上書きしています。
優先度で言うと、
style(!important無し) < css(!import付き)
だからです。なので、色を変更するには
// styleで元々の色が入っているのを、cssで!importandで無理やり新しい色にしている // ので、元のstyleを !importantにしてあげれば、cssより優先されて元の色になる、 const color = legend.style.backgroundColor; legend.style.setProperty('background-color', color, 'important');元のstyle(昔の色が入っている)に
!important
をつけてあげれば、
style(!important有り) > css(!import付き)
なので昔の色に戻ります。なんでこんな面倒な事になってるのか考察
おそらく、元の処理にはできるだけ手を加えずに、cssの変更だけで対応する作戦をとっているためと思われます。
CSSの変更だけなので、影響範囲を見た目だけに抑えて予期せぬ挙動の変更(ボタンが隠れて押せなくなるとかは考えられますが、、)が起きないようにしたり、チームのリソースの配分の都合等が考えられます。
まあ、↑で確認した通り、styleがかなり汚れるのでできれば避けた方が良いと私は思いますが、、、
まとめ
数時間で適当に作ったものなので色々荒いのですが、とりあえず動くので個人的には満足しています。(草色落ち着く、、、)
あと、GitHubがどうやって今回の見た目変更を実装したかが確認できたのも良かったです。きっと色々大変なんでしょうね、、
- 投稿日:2020-06-24T23:04:36+09:00
HTML canvasタグを使って描画
前置き
HTMLで碁盤を作れないだろうか?と考えた。
CSSだけでは線が引けないことで悩んでいたら、HTMLのcanvasタグを使うとjavascriptで描画が出来るということで試してみた。HTML側
HTMLの方は、canvasタグで描画される幅・高さを指定できる。
style属性を指定することで、背景色なども変更できる。app/html/kifu.html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>canvasで図形を描く</title> <script type="text/javascript" src="../js/kifu.js"> </script> </head> <body onLoad="kifu()"> <h2>canvasで碁盤を描く</h2> <canvas id="kifu" width="400px" height="400px" style="background-color:#f78740;"> 碁盤を表示するには、canvasタグをサポートしたブラウザが必要です。 </canvas> </body> </html>上のコードでは、幅(width)を400px、高さ(height)を400px、背景色を#f78740に指定している。bodyタグにあるkifu()はjavascript側の関数で、canvasタグの描画をする。
JavaScript側
描画のメインはこちら側に書く。
app/js/kifu.jsfunction kifu() { //描画コンテキストの取得 var canvas = document.getElementById('kifu'); if (canvas.getContext) { var context = canvas.getContext('2d'); context.strokeStyle = 'rgb(00,00,00)'; for (var x = 0; x < 8; x++) { for(var y = 0; y < 8; y++) { context.strokeRect(40+x*40, 40+y*40, 40, 40); } } var str = "_abcdefghi"; var game_record = []; canvas.addEventListener("click", function(e) { var stone_x = Math.round(e.offsetX / 40); var stone_y = Math.round(e.offsetY / 40); var stone_x_code = str[stone_x]; var stone_y_code = str[stone_y]; var stone_code = stone_x_code + stone_y_code; if (!(game_record.includes(stone_code))) { game_record.push(stone_code) if (game_record.length % 2 == 0) { context.fillStyle = "rgb(255, 255, 255)"; context.strokeStyle = "rgb(255, 255, 255)"; } else { context.fillStyle = 'black'; context.strokeStyle = 'black'; } context.beginPath(); context.arc(stone_x*40, stone_y*40, 19, 0, 2 * Math.PI, true); context.fill(); } console.log(game_record); }, false) } }上のコードで、碁盤の格子と黒・白交互に盤面に石が置ける。
碁盤は、正方形を8×8個書くことで生成。
碁石は、奇数手、偶数手で塗りつぶされる色を変更して円で表現。
石取りの判定が入っていないので、囲碁ではないですね。
五目並べには使えるかな(笑)後書き
ポートフォリオの題材に囲碁アプリを作りたかったんですが、棋譜再生方法、棋譜検討機能の実装で詰まってしまいました。
碁盤・碁石の描画だけはできるようになったので、アウトプットとして投稿します。
- 投稿日:2020-06-24T22:41:06+09:00
jqueryでsubmitボタンを毎回有効にする方法
テックキャンプでJavaScriptの学習中に学んだ事
jQueryを使って非同期通信の学習をしている最後に、送信ボタンを押してイベント発火後に
送信ボタンが無効化されているのを有効にする方法を記します。$('#hoge').prop('disabled', false);調べてみてわかった事は
- Railsのver5.0以降はdisabledがデフォルトで設定されている事(連打防止等の為)
- 他にも有効に出来る方法はある
例えば
$('#hoge').attr('disabled', false);自分のコードで試したが、どちらでも有効でした
今度は連打防止などの方法も調べて使え様にしていきます
非同期通信が自由に扱える様になると、少ないビューファイルの中に沢山の動的要素を
取り入れる事が出来て、かつレスポンスも早そうなので、プログラミングを学びたての自分でも
魅力的だなって感じました。難しいけど面白い
- 投稿日:2020-06-24T22:37:55+09:00
javascript 神経衰弱ゲーム(自分なりの解説)
今回も、書籍ゲームで学ぶJavascript入門で紹介されている
神経衰弱ゲームの知識定着のために自分なりに解説をしてみます。シャッフル処理
書籍の中では、
まず配列のprototypeの中に、
配列をシャッフルをする処理を記述してあります。(書籍と記述が異なります。)shuffle.jsArray.prototype.shuffle = function(){ var length = array.length; for(var i = length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var tmp = this[i]; this[i] = this[j]; this[j] = tmp; } return this; }トランプが10枚あるなら
10枚目のカードを、乱数で決めた2番目と入れ替える
9枚目のカードを、乱数で決めた5番目と入れ替える
8枚目のカードを、、、を繰り返して配列をバラバラにしている処理です。
ここでのthisは配列です。こちらでシャッフルします。
js.shuffte.jsvar cards=[] for(var i=1 ;i<=10;i++){ cards.push(i); cards.push(i); //1-10が2枚ずつの配列を cards.shuffle(); }トランプを表示させる
読み込み際に以下のhtmlを出力するようにjsで記述できればOKです。
card.html<table id="table"> <tr> <td class="card back"></td> <td class="card back"></td> <td class="card back"></td> <td class="card back"></td> <td class="card back"></td> </tr> <tr> <td class="card back"></td> <td class="card back"></td> <td class="card back"></td> <td class="card back"></td> <td class="card back"></td> </tr> <tr> <td class="card back"></td> <td class="card back"></td> <td class="card back"></td> <td class="card back"></td> <td class="card back"></td> </tr> <tr> <td class="card back"></td> <td class="card back"></td> <td class="card back"></td> <td class="card back"></td> <td class="card back"></td> </tr> </table>元々のhtmlは
base.html<body onload="init()"> <h2>経過時間:<span id="time">0</span>秒</h2> </body>前回の15puzzleにもありましたがこのような記述となります。
create.js//初期化 funcion init(){ var table = document.getElementById("table") for (var i =0; i<4; i++){ var tr = document.createElement("tr")// for( var j =0;j<5;J++){ var td=document.createElement("td");//tdを作成 td.className = "card back"; td.number = cards[i*5+j]; //0-19まで すでに数字はバラバラにしておく必要がある。 td.onclick = flip; //flip関数を設定。後ほど記述 tr.appendChild(td); // trのお尻にtdの閉じタグ } table.appendChild(tr); tableのお尻にtrの閉じタグ } }また開いたと同時に経過時間を表示させるために、
1秒おきに時間を取得して、開いた時間と引き算をして経過時間を表すtick関数も走らせます。tick.jsstartTime = new Date(); timer = setInterval(tick,1000); function tick(){ var now = new Date();//今時間をnowに入れる var elapsed = Math.floor((now.getTime()-startTime.getTime())/1000); document.getElementById("time").textContent = elapsed; } }トランプの裏返しの処理
oncilckで設定したflip関数を走らせます。
flip.jsfunciont flip(e){ var src = e.srcElement //押したtdの情報が取れます。 var num = src.number //initでnumberを値を設定しました。 src.className = "card"; src.textContent = num; //値をtextContentに入れて表示されます。 //1枚目 if(prevCard ==null){ //1枚目では未定義です。 prevCard = src; return; } //2枚目 if(preCard.number ==num){//番号が一致 if(++score ==10){ //1ペアになれば、1追加 clearInterval(timer) //10ペア完成したら タイマーを止める。 } prevCard = null;トランプをめくったときには、className="card"となり表を向いた表示がされます。
ただ数字が一致しない場合はclassName = "card back"と裏を向く処理が必要です。
preCardと構造が同じならif(preCard == num)があるのでそれのelseで、裏返しの処理となります。flip.jselse{ src.className = "card back"; 2枚目を裏に src.textContent =""; prevCard.className = "card back"; prevCard.textContent =""; prevCard = null;初期化 }プログラムは瞬時に働くので、これだと瞬時に表に戻ってしまいます。
前引いた番号を自分で覚えるのが神経衰弱の面白さなので、1秒間待ってから消す作業に移ります。flip.jssetTimeout(function(){ src.className = "card back"; src.textContent = ""; prevCard.className ="card back"; prevCard.textContent =""; prevCard =null; },1000) //1秒後に実行ところでsetTimeoutには戻り値があります。
戻り値timeId (数字)です。setTimeoutで実行した処理を止めるため
clearTimeout(timeId)でプログラムを止めることができます。setTimeoutが実際にプログラムが動きだす前から、timeIdを吐き出しています。
timeId = setTimeout(処理、100000)なら、100000を待たなくてもコードがそこを通れば
timeIdは出ます。書籍ではこのような記述がされています。
flip.jsflipTimer =setTimeout(function(){ src.className = "card back"; src.textContent = ""; prevCard.className ="card back"; prevCard.textContent =""; prevCard =null; flipTimer = null; },1000) //1秒後に実行中のfunctionが実行される前から、setTimeoutは戻り値を返しますので
flipTimerにはtimeIdを持つわけです。そして実際にfunctionが走ると、flipTimerをnullにされます。
flipTimerが値が持ってる1秒間は、他のトランプをクリックしても開かせない処理に。
flipTimerが値を持っていない時は、2枚は裏に戻ったので、クリックしたら開く処理に。flip.jsfunction flip(e){ var src =e.srcElement; //fliptimerが値があるのは、2枚開いてる間 or すでに表になってるカードを押してる if(flipTime ||src.textContent !=""){ return; //何もしない }
- 投稿日:2020-06-24T22:32:04+09:00
async/awaitで非同期処理の完了を待つ
①非同期処理をおこなう関数を宣言
const 関数名 = async () => { const message = 'GitのユーザーIDは'; const url = 'https://api.github.com/users/taizo-pro'②awaitをつけ、fetchが実行完了するまで次に進まなくさせる
// jsonにreturnされた値が入る const json = await fetch(url) .then(res => { console.log('非同期処理成功時のメッセージです') return res.json() }).catch(error => { console.error('非同期処理失敗時のメッセージです。', error) return null }); const username = json.login; console.log(message + username) } 関数名()
- 投稿日:2020-06-24T22:03:13+09:00
StreaminG!..Aston Villa vs Newcastle Live Stream
Newcastle vs Aston Villa: TV Channel, Live Stream, EPL Soccer Match Today. Aston Villa travels to Newcastle this evening as they continue their attempt to escape the Premier League relegation zone.
While Newcastle defeated a 10-man Sheffield United 3-0 in their last outing, Villa followed a 0-0 draw against the Blades with a 2-1 defeat by Chelsea.
Dean Smith's Villa sit 19th in the table on 26 points with time running out, communicating the Magpies are 13th on 38 points with a top-half finish not out of the question.
When is it and what time is kick-off?
The match will begin at 6 pm on Wednesday 24 June at St James' Park.How can I watch it online and on TV?
Who: Newcastle United vs. Aston Villa
When: Wednesday at 1 pm ET
Where: St. James' Park
TV: NBCSportsGold.com PL Pass
Online streaming: Catch select Premier League matches on fuboTV (Try for free. Regional restrictions may apply.)
Follow: CBS Sports AppThe match will be broadcast live on BT Sport 1, with coverage beginning at 5.45 pm. BT Sport subscribers can also stream the game on the BT Sport app.
Newcastle United's win lifted them to 10-12-8 (13th place with 38 points) while Aston Villa's defeat dropped them down to 7-18-5 (19th place with 26 points). We'll see if the Magpies can repeat their recent success or if Villa bounces back and reverse their fortune.
Predicted line-ups
Newcastle: Dubravka; Manquillo, Lascelles, Fernandez, Rose; Ritchie, Hayden, Shelvey, Saint-Maximin, Almiron; JoelintonAston Villa: Nyland; Konsa, Hause, Mings, Targett; Douglas Luiz, Grealish, Hourihane; El Ghazi, Samatta, Trezeguet
- 投稿日:2020-06-24T21:48:29+09:00
Live...Everton vs Norwich City Live Stream
Norwich City vs Everton: TV Channel, Live Stream, EPL Today Football Match. Everton travels to Norwich City for both teams' second match back after the resumption of the Premier League season.
Norwich suffered a 3-0 loss against Southampton over the weekend, while Everton drew 0-0 at home to Liverpool.
Both sides will be hoping they can make a return the scoresheet this time around, with Norwich aiming to reduce the six-point gap between themselves and safety and Everton aiming for a top-half finish.
When is it and what time is kick-off?
The match will begin at 6 pm on Wednesday, 24 June at Carrow Road.How can I watch it live stream and on TV?
Who: Norwich City vs. Everton
When: Wednesday at 1 pm ET
Where: Carrow Road
TV: NBCSportsGold.com PL Pass
Online streaming: Catch select Premier League matches on fuboTV (Try for free. Regional restrictions may apply.)
Follow: CBS Sports AppThe match will be broadcast live on BBC Two (first half), BBC One (second half), and the BBC iPlayer.
What is the team news?
Norwich will have Marco Stiepermann available again after having tested positive for Covid-19 earlier in the month. There were no injuries for Norwich in their last game so it should be a near-full squad for Farke.Everton is still without Yerry Mina, Fabian Delph, and Theo Walcott, while Morgan Schneiderlin has been sold to Nice. Djibril Sidibe is unlikely to be fit in time for this game.
Predicted line-ups
Norwich: Krul; Aarons, Godfrey, Klose, Lewis; McLean, Tettey; Buendia, Cantwell, Duda; Pukki.Everton: Pickford; Coleman, Holgate, Keane, Digne; Iwobi, Davies, Gomes, Bernard; Calvert-Lewin, Richarlison.
- 投稿日:2020-06-24T21:38:52+09:00
作って理解JavaScript:JOKE開発記その6 - break/continueおよびswitch
今回のスコープ
前回の予告通り、以下を実装します。
- breakとcontinue(JavaScriptはラベル指定breakができるがサポートしない)
- breakを踏まえたうえでのswitch文
またfor文で1つ目/2つ目/3つ目の式を省略した場合のテストをしていなかったのですが、特に2つ目は本文中でbreakしないと無限ループしてしまうので今ステップでテストすることにしました(なおステップ7段階の実装はもちろん省略に対応できていませんでした)
ステップ8段階のコードは以下にあります(push後に開発記書いててバグってる箇所に気づきました・・・)
https://github.com/junjis0203/joke/tree/step0008-fix1break/continueのための仕組み
前回、「if/while/forはジャンプする量を事前に計算できるがbreakはできない1、ほかの仕組みを作る必要がある」と書きましたが、その仕組みを実装しました。仕組みは以下のようになります。
- breakやcontinueを行える制御構造の開始時に「breakされたらここに飛ぶ、continueされたらここに飛ぶ」という情報をスタックに積む。このスタックをbreakableStackと呼ぶ2
- breakやcontinueが起きたらbreakableStackから情報を取り出して指定された位置にジャンプする
- breakやcontinueされることなく本文(forなどのStatement部分)を終えたらbreakableStackを一つ下ろす
forはかなりややこしいのでwhileで説明します。ステップ7と比べて
PUSH_BREAKABLE
とPOP_BREAKABLE
が追加されています。assembler.js抜粋case N.WHILE: { const exprInsns = []; assembleNode(node.expr, exprInsns); const stmtInsns = []; assembleNode(node.stmt, stmtInsns); const breakOffset = exprInsns.length + stmtInsns.length + 5; // to after POP_BREAKABLE const continueOffset = 0; makeInstruction(I.PUSH_BREAKABLE, {breakOffset, continueOffset}); embedInsns(exprInsns); makeInstruction(I.UNI_OP, {operator: '!'}); makeInstruction(I.JUMP_IF, {offset: stmtInsns.length + 2}); embedInsns(stmtInsns); makeInstruction(I.JUMP, {offset: -(exprInsns.length + stmtInsns.length + 2)}); makeInstruction(I.POP_BREAKABLE); } break;
PUSH_BREAKABLE
の引数であるbreakOffset
とcontinueOffset
は名前の通り「breakされたときの飛び先」「continueされたときの飛び先」です。またoffsetという名前が示す通りこの時点では「PUSH_BREAKABLE命令がある位置からの相対位置」です。VMでは
PUSH_BREAKABLE
を見つけると絶対位置を計算したうえでbreakableStackにpushします3。continueOffset
で条件分岐しているのは「breakはできるけどcontinueはできない」ものがあるからですが詳しくはswitch文のところで説明します。vm.js抜粋case I.PUSH_BREAKABLE: { // compute absolute address let ptrBreak = context.ptr + insn.breakOffset; let ptrContinue; if (insn.continueOffset != undefined) { ptrContinue = context.ptr + insn.continueOffset; } context.breakableStack.push({ptrBreak, ptrContinue}); } break;ともかくこれでbreakされたときにどこに飛べばいいのかわかるようになったので後はbreakableStackから取り出して絶対ジャンプをするだけです。
vm.js抜粋case I.BREAK: { const breakInfo = context.breakableStack.pop(); return {type: 'JUMP_ABS', ptr: breakInfo.ptrBreak}; }for文の修正
for文は1つ目/2つ目/3つ目の式を省略することができますが、ステップ7段階では考慮していなかったので対応しました(結果、ステップ7に比べてアセンブル処理が長くなりました)。ステップ7に追試として入れようかなと思いましたが2つ目の式省略はbreakがないと無限ループになってしまうのでステップ8としてテストすることにしました。
まあその部分は淡々と長いだけなので今回の主題であるbreak/continue時の飛び先の計算の話をします。
assembler.js抜粋let breakOffset = stmtInsns.length + 3; // to POP_SCOPE let continueOffset = stmtInsns.length + 1; // to expr3 if (node.expr2) { breakOffset += expr2Insns.length + 2; continueOffset += expr2Insns.length + 2; } if (node.expr3) { breakOffset += expr3Insns.length + 1; } makeInstruction(I.PUSH_BREAKABLE, {breakOffset, continueOffset});前回の復習として、for文は以下のような命令列にアセンブルされます。breakされた場合は「6.の無条件ジャンプ」の一つ先へ、conitinueされた場合は「5.の更新処理命令列」に飛ぶ必要があります。それを計算しているのが上記のコードです。4
- forの1つ目(初期化)の命令列
- forの2つ目(条件)の命令列
- 条件を否定し、否定したものがtrueならループ末尾へ
- ループ本体の命令列
- forの3つ目(更新処理)の命令列
- forの2つ目の先頭への無条件ジャンプ
switch文の実装
以上でbreakが実装できたのでようやくswitch文です。
まずはいつも通り仕様を確認。CaseClause : case Expression : StatementListなんとJavaScriptのcaseは「任意の式」が書けるようです!つまり以下のように書くこともできます!
switch (a) { case b + c: console.log('aはbとcを足したものに等しい'); break; }まあ「できる」と「意味がある」は別問題ですが。
JavaScriptではswitchに書かれた「式」とcaseに書かれているものは===
で比較されるので書けてもおかしくはないですね。アセンブル処理
ともかく構文解析を行い、アセンブル処理です。今までで最長。4
まず各部分のアセンブルを行います。
- switchの式をアセンブル
- 各caseについて式部分(Expression)を別命令配列にアセンブルしておく
- 同様に本文部分(StatementList)を別命令配列にアセンブルしておく
次に各部分の命令数を使って「条件にマッチした場合にどれだけジャンプすればいいか」のオフセットを計算します。
言葉だけでは絶対伝わらないので図解すると以下のようになります。ちなみに「case 1の本文」とかからジャンプがないのは間違いではありません。breakは「本文内のこと」なのでswitch自体に「本文実行したから末尾にジャンプ」という機能はありません。5assembler.js抜粋// calculate jump offset(any cool implementation?) for (let i = 0; i < labelInfoList.length; i++) { let insnsCount = 0; if (!labelInfoList[i].default) { for (let j = i + 1; j < labelInfoList.length; j++) { if (!labelInfoList[j].default) { insnsCount += labelInfoList[j].exprInsns.length + 3; } } insnsCount += 1; } for (let j = 0; j < i; j++) { insnsCount += labelInfoList[j].stmtsInsns.length; if (!labelInfoList[j].default) { insnsCount += 1; } } labelInfoList[i].stmtsOffset = insnsCount + 1; }最後に「受験分岐を行う命令列」「どの条件にもマッチしなかった場合のデフォルト(または末尾)へのジャンプ」「条件分岐の飛び先の命令列」を埋め込んでいきます。最終的に先に図解したような命令列になります。
assembler.js抜粋const labelInfoListWithoutDefault = labelInfoList.filter(c => !c.default); const labelInfoForDefault = labelInfoList.find(c => c.default); makeInstruction(I.PUSH_SCOPE); makeInstruction(I.PUSH_BREAKABLE, {breakOffset}); for (const labelInfo of labelInfoListWithoutDefault) { // dup switch.expr result for remain case makeInstruction(I.DUP); embedInsns(labelInfo.exprInsns); makeInstruction(I.BIN_OP, {operator: '==='}); makeInstruction(I.JUMP_IF, {offset: labelInfo.stmtsOffset}); } if (labelInfoForDefault) { // jump to default label if no match makeInstruction(I.JUMP, {offset: labelInfoForDefault.stmtsOffset}); } else { // jump to switch end let offset = labelInfoList.reduce((sum, curr) => sum + curr.stmtsInsns.length + 1, 0); offset += 1; makeInstruction(I.JUMP, {offset}); } for (const labelInfo of labelInfoList) { if (!labelInfo.default) { // pop duplicated switch.expr result makeInstruction(I.POP); } embedInsns(labelInfo.stmtsInsns); } makeInstruction(I.POP_BREAKABLE); makeInstruction(I.POP_SCOPE); // pop unused switch.expr result if (!labelInfoForDefault) { makeInstruction(I.POP); }テストでは以下のように「わざとbreak入れないで次のcase 3部分が実行(出力)されるか」を確認していますが、このほぼバグな動作を実現することがこれほど大変とは思いませんでした(笑)
step0008_21.jsswitch (a) { case 1: console.log('Apple'); break; case 2: console.log('Banana'); // fallthrough(intendedly) case 3: console.log('Cherry'); break; default: console.log('Other'); break; }switch文でのcontinue
さて伏線しておいた「breakはできるけどcontinueはできない」ものの話です。具体的にはswitchがそれに該当します。以下のようなプログラムを考えてみましょう6。コメントにあるようにswitch文内に書かれているcontinueはswitch文に作用するのではなくfor文に作用する必要があります。
step0008_22.jsfor (let i = 1; i <= 5; i++) { switch (i) { case 2: console.log('foo'); continue; // goto next for case 4: console.log('bar'); break; // goto switch end } console.log(i); }これを実現するために、
PUSH_BREAKABLE
を再掲すると以下のようにcontinueOffset
は「設定されていたら計算する」ようにしてあります。switchに対応するPUSH_BREAKABLE
ではこのプロパティは設定されていません(つまりundefined
)vm.js抜粋case I.PUSH_BREAKABLE: { // compute absolute address let ptrBreak = context.ptr + insn.breakOffset; let ptrContinue; if (insn.continueOffset != undefined) { ptrContinue = context.ptr + insn.continueOffset; } context.breakableStack.push({ptrBreak, ptrContinue}); } break;先ほどは飛ばしたcontinue処理ではbrekableStackをたどって
ptrContinue
が設定されているものを使います。なお無限ループになっていますが別途「ptrContinueのあるbreakInfoが積まれる」ことは確認しているので実際に無限ループすることはありません(はず)vm.js抜粋case I.CONTINUE: while (true) { const breakInfo = context.breakableStack.pop(); if (breakInfo.ptrContinue) { return {type: 'JUMP_ABS', ptr: breakInfo.ptrContinue}; } }言語機能以外の改造
breakが出てくると命令ポインタの動きが頭の中で追いきれなくなってくるのでデバッグ情報として命令ポインタも出すようにしました。
ここら辺、「どのinsnsListを実行しているのか7」のような情報もいるかなと思いますが未対応です。また、「構文解析結果やアセンブル結果は見たいけど実行はしなくていい」のようなdry runみたいな機能もいるかなと思っていますがいいオプション文字列を思いついていないため未対応です。その他
最終目標のセルフホスティングについて、「何ができたらセルフホスティングできたと言えるか」を定義していなかったのでテストを追加しました。このテストが通ったらセルフホスティング完了とします。8
step0100.jsimport JokeEngine from 'joke'; const program = ` function hello(callback) { console.log("Hello"); callback(); } hello(() => { console.log("World"); }); `; const joke = new JokeEngine(); joke.run('<program>', program);あとがき
以上、今回はbreakとcontinueの仕組みをメインに説明してきました。これで制御構造はひとまず終わりです9。
次は配列かなと思っていますが、メソッドのことを考えると先にオブジェクトとprototypeを作るべきか?ということも検討中です。
ちなみにCRubyやCPythonはbreakに対して一旦ブロック末尾に対応するラベルに飛ぶようにしておき、後からラベルとbreakが書いてある位置の距離を計算してたはずです(末尾の位置はまだ決まってないのでbreakを見つけた時点では計算できない)。ラベル絶対使いたくないマンというわけではないですがJOKEではこの方法は採用しませんでした。 ↩
ちなみに仕様ではwhile/forなどのIterationStatementとswitch(SwitchStatement)はBreakableStatementとしてまとめられています。 ↩
このようにしているのは
PUSH_BREAKABLE
を出力する時点では相対位置しかわからない(whileのstmtなどは別の命令列配列に書き込ませた後マージしている)ためですが書いててやはりラベル埋め込み後から計算法の方が楽かもとも思ってきました(笑) ↩else ifについてはJavaScriptでは「else節の文」として再帰処理されるためシンプルです。 ↩
これにより無数のバグが生まれたのはまた別のお話。 ↩
意味があるかは別として。ふと思いついてしまったのでテストしました。 ↩
すでにコミットしているので、セルフホスティング完了まで延々とテストが失敗します(笑) ↩
例外処理はクラスを作るまでできない、クラスは実質式ですし、for-ofは配列と合わせて実装するつもりです。 ↩
- 投稿日:2020-06-24T21:17:26+09:00
【javascript】タグ付きテンプレートリテラル
タグ付きテンプレートリテラルとは
関数を使ってタグ付けしたテンプレートリテラルのことです。
タグ付のやり方
テンプレートリテラルの先頭にその関数の名前を配置します。
myTag`my string`上の例では
my string
を関数myTag
でタグ付けしています。
myTagb
関数の呼び出しではテンプレートリテラルが引数として渡されます。テンプレートリテラルを関数でタグ付けすると、テンプレートリテラルが文字列部分と補間部分に分解され、タグ関数に別々の引数として渡される。
引数 説明 最初の引数 文字列部分がすべて含まれた配列 以降の引数 補間される値 function tagFunction( stringPartsArray, interpolatedValue1, ..., interpolatedValueN) { }しかしこれらの引数を明示的に記述する必要はありません。
テンプレートリテラルが分解されて関数に渡される仕組み
これらの要素が分解されて関数に渡される仕組みを知るために以下の例を見てみましょう。
function logParts() { let stringParts = arguments[0]; let values = [].slice.call(arguments, 1) console.log('Strings', stringParts) console.log('Values', values) } logParts `1${2}3${4}${5}`; //Strings:["1", "3", "", "", raw: Array(4)] //Values:[2, 4, 5]上記では
arguments
オブジェクトを使って文字列部分と補間される値を解析しています。arguments は、関数へ渡された引数を含む、関数内のみアクセス可能な 配列様 (Array-like) オブジェクトです。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/arguments
let stringParts = arguments[0];
で最初の引数を取得しています。
let values = [].slice.call(arguments, 1)
で2つ目以降のパラメータがずべて含まれた配列を取得しています。
argumentsオブジェクトは配列ではなく配列風オブジェクトです。配列風オブジェクトを配列にするには[].slice,call()
を実行します。arguments オブジェクトは Array ではありません。これは Array と似ていますが、length 以外のどんな Array のプロパティも持ちません。たとえば、これは pop メソッドを持ちません。しかしながら、これは本当の Array に変換できます。
Stringsに文字列
'1'
と'3'
のあと、空文字が含まれている。これは4と5の夜に補間される洗が隣り合わせで、間に他の文字列部分が含まれていない場合、それらの間には空の文字列が存在していると解釈されるためです。
これは文字列の先頭と末尾にも当てはまります。//---------------------------- //文字列 "1" "3" "" "" //値 2 4 5 //----------------------------このため常に最初と最後の要素は文字列で、値はそれらをつなぎ合わせる存在です。
つまり単純なreduce関数でそれらの値を処理できるということです。function noop(){ let stringParts=arguments[0]; let values=[].slice.call(arguments,1) return stringParts.reduce(function(memo,nextParts){ //shiftメソッドは配列から最初の値を取り出す。 return memo+String(values.shift())+nextParts }) } noop`1${2}3${4}${5}`;//12345
- 投稿日:2020-06-24T21:02:21+09:00
CORS(preflight request)にハマったけど解決した話
CORS(preflight request)にハマったので、解決方法を備忘録として残しておきます。
エラーが起きた場面
異なるドメインからHttpリクエストを送る場合は、CORSに注意だよなぁ。
サーバー側のレスポンスで、ヘッダーをつけてあげれば良いんだろう。
簡単じゃん。クライアント側
何かしらのデータをjasonでPOSTする。
var xmlhttp = new XMLHttpRequest(); xmlhttp.open("POST", API_ENDPOINT); xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); xmlhttp.send(JSON.stringify(data));APIサーバー側
サーバー側では、レスポンスのヘッダーを付けてあげる。
func (a *API) HandleFunc(w http.ResponseWriter, r *http.Request) { //略... w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") //略... fmt.Fprint(w, "ok") }結果、エラー...!
当然、結果が正常に返ってくると思いきや
プリフライトリクエスト?がどうのこうのといったエラーが出た。。。なんやこれ。。。Access to XMLHttpRequest at 'http://localhost:8080/api' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.CORS(preflight request)エラー解決
preflight requestとは?
リクエストによっては CORS プリフライトを引き起こさないものがあります。これをこの記事では「単純リクエスト」と呼んでいますが、 (CORS を定義している) Fetch 仕様書ではこの用語を使用していません。 「単純リクエスト」は、以下のすべての条件を満たすものです。
・許可されているメソッドのうちの一つであること。
GET
HEAD
POST
・ユーザーエージェントによって自動的に設定されたヘッダー (たとえば Connection、 User-Agent、 または Fetch 仕様書で「禁止ヘッダー名」として定義されているヘッダー) を除いて、手動で設定できるヘッダーは、 Fetch 仕様書で「CORS セーフリストリクエストヘッダー」として定義されている以下のヘッダーだけです。
Accept
Accept-Language
Content-Language
Content-Type (但し、下記の要件を満たすもの)
DPR
Downlink
Save-Data
Viewport-Width
Width
・Content-Type ヘッダーでは以下の値のみが許可されています。
application/x-www-form-urlencoded
multipart/form-data
text/plain
・リクエストに使用されるどの XMLHttpRequestUpload にもイベントリスナーが登録されていないこと。これらは正しく XMLHttpRequest.upload を使用してアクセスされます。
・リクエストに ReadableStream オブジェクトが使用されていないこと。
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#Preflighted_requests上記の条件を満たさないものはpreflight requestがメインのリクエストの前に行われます。
今回は、POST
ですが、"Content-Type", "application/json"
をヘッダーに付けているので、プリフライトリクエストが起きました。
画像の最初のリクエストがpreflight requestです。
ここでは、実際にはOPTIONS
メソッドがpreflight requestとして走ります。
解決方法
OPTIONS
メソッドの時は、http.StatusOK
を返すようにしたら解決しました!func (a *API) HandleFunc(w http.ResponseWriter, r *http.Request) { //略... w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } //略... fmt.Fprint(w, "ok") }
- 投稿日:2020-06-24T19:20:09+09:00
任意の深さのオブジェクトからパスを生成するコード
メモ
入力
const list = { top: [], recentArticle: [], user: { article: { top: [], edit: [], }, articleList: [], follow: [], follower: [], }, setting: [], signin: [], signup: [], }出力
[ '', '/recentArticle', '/user/article', '/user/article/edit', '/user/articleList', '/user/follow', '/user/follower', '/setting', '/signin', '/signup' ]コード
const getPathList = (list) => { const _getPathList = (parentPath, obj) => { return Object.keys(obj).map(key => { const currentPath = `${parentPath}/${key}` if (Array.isArray(obj[key])) { return currentPath.replace("/top", "") }else{ return _getPathList(currentPath, obj[key]) } }) } return _getPathList("", list).flat(Infinity) }
- 投稿日:2020-06-24T18:59:07+09:00
プログラミングサークルでの活動記録
この記事について
コンピュータ研究会セキュリティ部門でやったことを、まとめながらアウトプットのために書きます。
毎週金曜日にどんどん追加していきます。6月19日
活動内容:
https://google-gruyere.appspot.com/ のpart3, part4 までやりました。課題:Path Traversal Attack を体験できるウェブアプリケーションの作成
ディレクトリ構成. ├── templates │ └── index.html ├── a.txt ├── b.txt ├── c.txt ├── pass.txt └── app.pyindex.html<!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>Hello Jinja2</title> </head> <body> <h1>Path Traversal Attack を体験できるウェブアプリケーション</h1> <p>下のフォームに表示したいファイルのファイル名を入力してください</p> <p><strong>a.txt</strong> <strong>b.txt</strong> <strong>c.txt</strong> の中から選べます。</p> <form action="/" method="POST" enctype="multipart/form-data"> <div> <label for="name">ファイル名:</label> <input type="text" id="name" name="name" placeholder="名前"> </div> <div> <input type="submit" value="送信"> </div> </form> <p>{{data}}</p> <br><br><br> <h4>解説</h4> <p>このサイトでは、受けとたったファイル名を持つファイルをディレクトリ内で探し、そのまま返しています</p> <p>よって、入力が予想されないファイル名が入力されてしまった時極秘ファイルを返してしまう可能性があります。</p> <h3>pass.txtと入力してみましょう</h3> </body> </html>a.txtこれは a.txt の中身です。b.txtこれは b.txt の中身です。c.txtこれは c.txt の中身です。pass.txt貴重なパスワードのデータを抜き取ることができました。app.py# -*- coding: utf-8 -*- from flask import Flask, render_template, request app = Flask(__name__) @app.route('/', methods=['POST']) def post(): name = request.form.get('name') data = "" try: f = open(name) data = f.read() f.close() except: pass return render_template('index.html', data = data) if __name__ == '__main__': app.run()ローカルサーバーを立てれば良いだけだったので、Flaskを使って書きました。とんでもなく簡単に実装できたので、さすがFlask!って感じでした。
、、てかこんなアホな脆弱性のある実装する人本当にいるのでしょうか、、?6月12日
活動内容:
https://google-gruyere.appspot.com/ のpart0, part1, part2 までやりました。学んだこと:
XSS(クロスサイトスクリプティング)には、反射型、蓄積型、DOM based xssなど様々な種類がある。
XSSの対策、サニタイジング(エスケープ)をする。課題: DOM Based XSS を体験できるウェブページの作成
xss.html<html> <title>DOM Based XSS</title> <h1>DOM Based XSSが体験できるサイト</h1> Hi <script charset="UTF-8"> var pos=document.URL.indexOf("name=")+5; document.write(unescape(document.URL.substring(pos,document.URL.length))); </script> <br><br> <p>このページのurlの最後にパラメータとして名前をもたせると、動的にhtmlを書き換えます</p> <p>(例) 〜〜xss.html?name=Taro</p> <p>一番上のHiの後に名前が表示されたと思います。</p><br> <P>しかしこのサイトではパラメータに悪意のあるスクリプト入れられたときにそれを実行してしまいます。</P> <p>(例) 〜〜xss.html?name=<script>alert("Your PC was broken!!")</script></p> <P>alertができるということは、実際に悪影響を及ぼすスクリプトを実行させられてしまいます。</P> </html>
- 投稿日:2020-06-24T17:32:54+09:00
現代のフロントエンド開発シリーズ(8) - Babelについて
ES2015はECMAScript 2015の略です。これはJavaScript言語の詳細を実装する方法に関する仕様であり、以前のバージョンよりもはるかに簡潔な構文と関数を備えています。
開発者は、古いJavaScriptの作成に長い間うんざりしていて、ES6が提案する構文は結構開発者に愛されました。
ドラフトからファイナライズ、ブラウザー実装までの一連の仕様について話しましたが。
多くの場合、これらの構文を直接使う場合、ブラウザーのサポートが必要になり、古いブラウザーなど使うと確実にエラーが出るでしょう!ユーザーはJavascriptがES5で記述されているかES6で記述されているかをとくに気にしませんが。ES6の構文を使ってエラーが出る場合、ユーザーと上司に怒られるでしょう
これらの問題を解決するために、「babel」が登場しました。簡単に言うと、babelはJavascriptをAST(抽象構文ツリー)に変換し、ASTをブラウザーがプラグインを導入して、ブラウザーが理解できるコードに変換できるパーサーです。
ここにアクセスして、詳細な履歴を確認できます。
2015年もReactが徐々に人気になったため、ES6をサポートするだけでなく、babelにはJSXの恵みもあり、Reactがすぐに人気を博しています。
Babelの原理
Babelの実装は、主にJavaScriptを抽象構文ツリー(AST)に変換し、さまざまなプラグインを使ってES5のコードを変換することです。
完全なパーサーを作成することは簡単な作業ではありません。まず、JavaScript全体の構文と構造、およびコンパイラーと構文分析に関するさまざまな知識を実装する必要があります。
ただし、AST抽象構文ツリーの概念を知っている開発者は、さまざまな場所で使用でき、もちろん自分でbabelプラグインを作成することもできます。
AST(抽象構文ツリー)とは
抽象構文ツリーの構成については詳しく説明しませんが、すべてのプログラミング言語は、キーワード、識別子およびその他のトークンに構成されています。たとえば、構文ツリーに変換された
console.log('hello world');
というコードは次のようになります(ASTエクスプローラーで利用可能):{ "type": "Program", "start": 0, "end": 40, "body": [ { "type": "ExpressionStatement", "start": 0, "end": 27, "expression": { "type": "CallExpression", "start": 0, "end": 26, "callee": { "type": "MemberExpression", "start": 0, "end": 11, "object": { "type": "Identifier", "start": 0, "end": 7, "name": "console" }, "property": { "type": "Identifier", "start": 8, "end": 11, "name": "log" }, "computed": false }, "arguments": [ { "type": "Literal", "start": 12, "end": 25, "value": "hello world", "raw": "'hello world'" } ] } } }
console.log('')
はExpressionです。console.logはCall Expressionです。つまり、特定の関数を呼び出すことを意味し、.
の操作はMember Expressionです。console
とlog
はどちらも識別子(identifier)で、hello world
はLiteralです。この構文ツリーを使用すると、コードに対してさまざまな複雑な操作を実行できます。
const { name, age } = person; // es6 var name = person.name; // es5 var age = person.age; // es5es6の構文を古いブラウザで正しく機能させるために、最初に「babel」を導入してJavascriptをコンパイルし、ブラウザーを対応できるコードに変換します。
これは不思議に聞こえますね
「Javascriptでパーサーを作成し、JavascriptをJavascriptに変換する?」実際、これはJavaScriptのせいにすることはできません。
ユーザーはブラウザーでWebページを表示しています。もちろん、ユーザーに強制的に更新を強制したり、必ず「Chrome使って」みたいな要求をしたりすることはできません。
バックエンドサーバーとは異なり、アップグレードしたい場合は、そのままアップグレードすればいいです。抽象構文ツリーを作成すると、コードを簡単に操作できます。たとえば、ブラウザをサポートするには、すべてのタイプの
VariableDeclaration
をvar
に変更する必要があります。ツリー内のすべてのVariableDeclaration
を探して、kind
をvar
に変更するだけです。JSXを
React.createElement
に変換する方法babel-plugin-transform-jsxは、jsx構文をASTに変換するのに役立ちます。 Reactのjsxは、plugin-transform-react-jsxを使用してjsxをReact.createElementに変換します。
function MyComponent() { return React.createElement("div", null, React.createElement("span", null, "hello world")); } // equal function MyComponent() { return <div> <span>hello world</span> </div> }もちろん
React.createElement
に限定する必要はありません。たとえば、VirtualDOMシステムとレンダリングメカニズムを記述したり、jsx
を使用したりすることもできます。Babelを使用すると、一部のレガシーコードを変換できるだけでなく、コードを分析することもできます。
DIYバベルプラグイン
独自のバベルプラグインを作成するには、babel-handbookに参考してみましょう。
たとえば、すべてのコードを
console.log
で最近書き込んだLogger.log(args)
に置き換えますが、簡単に見つけてエディターで置き換えることができるでしょう。今回エディターを使わず、Babelを使いましょう!console.log('This is error!');これは私のソースコードです。
console.log
が使用されているすべての場所が、先ほど書いたLogger
に置き換えられることを願っています。const t = require('babel-core').types; const visitor = { Expression(path) { if (t.isCallExpression(path.node)) { if (t.isMemberExpression(path.node.callee)) { const { callee } = path.node; const args = path.node.arguments; if ( t.isIdentifier(callee.object) && t.isIdentifier(callee.property) && callee.object.name === 'console' && callee.property.name === 'log' ) { const callExpr = t.callExpression( t.memberExpression(t.identifier('Logger'), t.identifier('log')), args ); path.replaceWith(t.inherits(callExpr, path.node)); } } } }, } module.exports = (api, state) => ({ name: 'transform-console-log', visitor });
console.log()
はメンバー呼び出し式であるた、現在のノードがconsole.log
の呼び出しである場合、Logger.log
の式に置き換えられると判断します。Logger.log('There is error!');まとめ
今回Babelのことについて紹介しました、Babel Pluginを使って、よりきれいなコードがかけるので、ぜひ理解してみましょう!
- 投稿日:2020-06-24T15:25:26+09:00
GoogleChatのグループチャットのスレッド一覧リンクを表示するブックマークレット(コピペでOK)
GoogleChatのグループチャットのスレッド一覧リンクを表示するブックマークレット(コピペでOK)
GoogleChatには、
グループチャットでスレッドを作る機能はありつつも、
あくまでチャットなので、一覧化レイアウトがありません。
なので、簡単にログが流れて行ってしまいます。そこで、今表示しているグループチャットのスレッドを
一覧表示するブックマークレットを作成しました。
需要があるかわかりませんので、使用にも部品取り(何かのヒントに)
にもご活用いただければ幸いです。使用方法
適当なブックマークを作り、そのURLに下記のJavascriptを
貼り付けていただき、GoolgeChatでグループチャットを開き、
このブックマークレットをクリックすれば起動します。※諸注意:
PC版Chromeで動作確認しています。
Googleの仕様が変更となった場合には使えません。
2020年6月24日現在の仕様となります。ポップアップHTMLで一覧表示する版
javascript:(function(){links=document.querySelectorAll("c-wiz[data-topic-id]");kekka="";for(i=0;i<links.length;i++){for(z=0;z<links[i].attributes.length;z++){ if(links[i].attributes[z].name=="data-topic-id"){ if(kekka.indexOf(links[i].attributes[z].value) == -1){kekka+="<tr><td style='font-size:10px;'>"+location.href+"/"+links[i].attributes[z].value + "</td><td style='font-size:10px;'>" + links[i].firstElementChild.innerText + "</td></tr>";}else{}}else{}; }} myWindow = window.open("", "myWindow", "width=600,height=400");myWindow.document.write("<table>"+kekka+"</table>");})()
コンソールに表示する版
javascript:(function(){links=document.querySelectorAll("c-wiz[data-topic-id]");kekka="";for(i=0;i<links.length;i++){for(z=0;z<links[i].attributes.length;z++){ if(links[i].attributes[z].name=="data-topic-id"){ if(kekka.indexOf(links[i].attributes[z].value) == -1){kekka+=location.href+"/"+links[i].attributes[z].value+"\t"+links[i].firstElementChild.innerText;}else{}}else{}; }} console.log(kekka);})()
簡易解説
すべてjavascriptです。Jqueryは使いません。
難しい事はしておらず、Googleのコーディング仕様を
把握するのが少し時間がかかるというくらいですw
links=document.querySelectorAll("c-wiz[data-topic-id]");
セレクターでスレッドの区切りとなるタグを取得します。
links[i].attributes
該当のタグの属性を取得します。
if(links[i].attributes[z].name=="data-topic-id")
該当のタグ/属性は複数存在するため、forとifを使って
目当ての属性を抜き出します。
※data-topic-idに、スレッドのIDが格納されています。
if(kekka.indexOf(links[i].attributes[z].value) == -1)
.valueで属性の値を取得していますが、
タグが2つずつ存在し重複があるため、「値を格納する変数」の
中をindexOfで確認して、存在しなければ書き出しする事にしました。
links[i].firstElementChild.innerText
これは各スレッドに対する最初の書き込みを取得しています。
firstElementChildで最初の子要素を見つけています。
- 投稿日:2020-06-24T15:21:41+09:00
【Vue】学習開始3週目で覚える内容
3週目で学ぶべきこと
- 双方向データバインディング
- 修飾子
コンポーネント間バインディング
- 入力値を
$event.target.value
で受け取るprops
,$emit
を使用してデータバインディングを行う◆ 親コンポーネント
App.vue<template> <!-- v-modelで入力された値を"Child.vue"に"$event"として共有する --> <Child v-model="sample"></Child> </template> <script> export default { data() { return { //sample:属性名 YES:初期値 sample: "YES" } } }; </script>◆ 子コンポーネント
Child.vue<template> <!-- "親コンポーネントのv-model"で入力された値を受け取る --> <input :value="value" @input="$emit('input', $event.target.value)" > <!-- "子コンポーネントの入力値を"$emit"を使用して、"親コンポーネント"と共有する --> <!-- input:属性値 $event.target.value:入力値 --> </template> <script> export default { //"親コンポーネント"の"v-model"入力値 props: ["value"] } <script>テキストボックスバインディング
p style="white-space: pre-line;"
を使って、空白・改行を反映するtextareaタグ
+v-model
でテキストボックスを作成するplaceholder
は、入力欄の初期値
を指定するApp.vue<template> <div> <!-- "textarea" + "v-model"で、入力欄作成 --> <textarea v-model="sample" placeholder="入力欄"></textarea> <!-- p style="white-space: pre-line;"で空白・改行を反映する --> <p style="white-space: pre-line;">{{ sample }}</p> </div> </template> <script> export default { data() { return { //sample:属性名 sample: "" } } }; </script>単体チェックボックスバインディング
input type="checkbox"
で、チェックボックス作成- v-modelで指定するプロパティは、
string or numberを選択するboolean型
App.vue<template> <div> <!-- "checkbox"をセットする --> <input type="checkbox" id="sample" v-model="sample"> </div> </template> <script> export default { data() { return { //string or numberを選択するboolean型 sample: false } } }; </script>複数チェックボックスバインディング
- labelタグ:
label forの属性値
と、inputタグのid属性値
が同じ場合、紐付けられる- 複数選択のチェックボックスの場合、dataプロパティ値(sample)は、
配列
をセットするApp.vue<template> <div> <!-- "YES"を選ぶcheckboxをセットする --> <input type="checkbox" id="1" value="YES" v-model="sample"> <label for="1">YES</label> <!-- "NO"を選ぶcheckboxをセットする --> <input type="checkbox" id="1" value="NO" v-model="sample"> <label for="1">NO</label> <!-- "checkboxで選択した値"を出力する --> <p>{{sample}}</p> </div> </template> <script> export default { data() { return { //checkboxで選択した値を"配列"として受け取る sample: [] } } }; </script>ラジオボタンバインディング
inputタグのid属性値
とlabel for
の属性値は同じ値
をセットすることで紐付けるinput type="radio"
でラジオボタン
を作成する- 各々の選択肢に
同じv-model
を設定することで、選択可能になるApp.vue<template> <div> <!-- "YES"を選択するラジオボタンをセットする --> <input type="radio" id="1" value="YES" v-model="sample"> <label for="1">YES</label> <!-- "NO"を選択するラジオボタンをセットする --> <input type="radio" id="2" value="NO" v-model="sample"> <label for="2">NO</label> </div> </template> <script> export default { data() { return { //sample:属性名 YES:初期値 sample: "YES" } } }; </script>セレクトボックスバインディング
v-for="引数 in 配列"
でレンダリングを実施するmultiple
は複数選択
を許容するApp.vue<template> <div> <!-- セレクトボックスの入力値とdataプロパティ属性値を"v-model"で関連付ける --> <!-- "multiple"は、複数選択を許可する --> <select v-model="sample" multiple> <!-- v-forディレクティブで"リストレンダリング"を実施し、keyに"sample"をセットする --> <option v-for="sample in samples" :key="sample">{{sample}}</option> </select> <!-- セレクトボックスで選択した内容を出力する --> <p>{{sample}}</p> </div> </template> <script> export default { data() { return { //v-forディレクティブで使用する"配列"を作成する samples: ["犬", "ネコ", "ウサギ"], //セレクトボックスの"初期値"を設定する sample: "犬" } } }; </script>修飾子
◆ lazy修飾子
changeイベント
後に、データを反映させる
- デフォルトでは、v-modelは
inputイベント
後に反映させるApp.vue<template> <div> <!-- lazy修飾子によって、"changeイベント"後にデータ反映 --> <input v-model.lazy="sample"> </div> </template>◆ number修飾子
- ユーザの入力を
number型
に自動変換させるApp.vue<template> <div> <!-- number修飾子によって、入力データを"number型"に変換 --> <input v-model.number="sample" type="number"> </div> </template>◆ trim修飾子
- ユーザの入力から
空白を自動でカット
する<pre>タグ
は空白や改行をそのまま表示するApp.vue<template> <div> <!-- trim修飾子によって、入力データの空白を自動でカットする --> <input v-model.trim="sample" type="text"> <!-- "preタグ"によって、trim修飾子の"空白カット"をそのまま表示する --> <pre>{{ sample }}</pre> </div> </template> <script> export default { data() { return { //sample:属性名 default:値 sample: "default" } } }; </script>同シリーズ
参考文献
- 投稿日:2020-06-24T15:02:41+09:00
Openbase.ioって言うサイトが面白い!
この記事は何ですか?
openbase.ioっていうサイトをたまたま見つけて、面白かったのでそのサイトの紹介記事です!
注意として、openbase.ioは出来て間もないサイトなので、それを踏まえた上でこの記事を読んで欲しいです?因みに、筆者はopenbase.ioと何の関係も無いのです。
なので、この記事はあくまで openbase.io に魅せられたユーザーの個人的な記事です。Openbase.ioってどんなサイト?
JavaScriptのモジュールを検索したり、比較したりなどして、モジュール選択をサポートしてくれるサイトです!
以下の画像は、openbase.ioのホーム画面のスクショですが、凄くデザインが良いですよね。個人的に好きなポイント
個人的に好きなポイントを列挙していきます。
モジュールの情報が一目でわかる!
以下の画像は、React.jsをopenbase.ioで表示したときの画像で、React.jsの情報を一目で分かるように表示してくれています。
ここで嬉しいのが、issues
やPull Request
などのリポジトリ情報も表示してくれます。これのおかげで、そのモジュールがちゃんとメンテナンスされているかとか分かって、凄く良いなと思ってます。あと、グラフなどを用いて分かりやすくしてくれているので、モジュールのリポジトリをそのまま見るより、凄く理解しやすいです。
モジュールを比較しながら探せる!
これ結構嬉しいですよね。サービスでの技術選択でどのフレームワークを使おうかと悩んでいる時とかに、こうやって比較してくれると、選定がやりやすくなると思います。
これと同じような比較サイトとしてnpm trendsなどがありますが、情報が少なくて、ちょっと物足り無い感じがしますし、openbase.ioは前述した通り、リポジトリ情報とかも見ながら比較できるので、情報量の多いopenbase.ioが使いやすいなと個人的に思ってます。
openbase.ioの方で、モジュールをカテゴライズしてくれているので、そこら辺も他のサイトよりモジュールを比較しやすい要因だと思います。
以下の画像は、MVC Frameworkのカテゴリーページです。
やっぱり、Vue.jsは強いですね?
チュートリアル動画などを見つけることが出来る!
これ、はじめ見たときは感動しました。モジュールのチュートリアル動画とか結構探すの面倒くさいですよね。
チュートリアル動画を出しているようなモジュールは少ないと思いますが、有名なモジュールには大抵あると思うので、そこらあたりを動画を見ながら比較できる点はすごく良いと思います。以下は、React.jsのチュートリアル動画のページ
依存関係を分かりやすく表示
これは以下の画像を見てもらえば分かると思います。
依存関係とか調べるのめっちゃ怠いのですが、openbase.ioなら全然苦じゃないですね。
むしろ、私は楽しくてめっちゃ見てました。このサイトで一時間くらい余裕で潰せる?あとがき
とりあえず、このくらいでopenbase.ioの紹介は終わろうと思います。
あまり、語りすぎるとよくないですからね。はぃ。
経験豊富なエンジニアの皆さんは、話が長過ぎて部下に嫌われたりしてるので、要注意ですよ?また、UIなども綺麗にかっこよくデザインされていて、個人的に凄く好きなデザインでした。デザインの勉強とかするのに、参考にしてみもいいかもしれません。
最後に、この記事で紹介してない魅力的な所もいっぱいあるので、是非ご自身の目で確かめてください。
見るだけでも十分楽しいですよ。タブン?ここまで読んでくれてありがとうございます。
それでは、また?
- 投稿日:2020-06-24T14:13:25+09:00
【Rails】JavaScriptでフォームを追加する方法
はじめに
以前、単一ページのフォームで複数のテーブルにデータを保存する方法を書きましたが、その続きとしてJavaScriptを利用してフォームを追加・削除できるようにする方法を書いていきます。
以前の記事 → https://qiita.com/koki_73/items/bc4ca80ab43e84d9704f完成イメージは以下のような感じです。
今回はフォームを最大で5つまで作成できるようにしていきます。
なお、railsのバージョンは5.2.3で、ビューファイルはhamlとJqueryを使います。概要
追加・削除の概要を簡単に説明しておきます。
- フォームに識別用の番号を振る
- 追加するフォームに渡す番号を配列で用意する
- 追加のときは配列の数字を使って新しい識別番号のフォーム作成し、削除のときは配列に数字を追加する。
では追加、削除のやり方をそれぞれ説明します。
フォームの追加
まずはどんな流れでフォームを追加すればよいか確認してみます。
- 追加するフォームのまとまりにクラスとインデックス番号を設定する(HTMLファイル)
- 追加ボタンをクリックしたときにイベントを発火させ、新しいインデックス番号を使って新しいフォームを追加する。(JSファイル)
追加は特に難しいことはありません。
ではコードをみていきましょう!HTMLファイル
viewファイル= form_with(model: @post, local: true) do |f| .post-area .post-area__title 投稿内容 .post-area__form = f.text_field :content -# ↓のクラスの子要素としてフォームを追加します .tag-area = f.fields_for :tags do |tag| -# ↓このクラスにindex番号を振り、フォームを識別できるようにします .js-file-group{ data: {index: "#{tag.index}"} } .tag-area__title タグ .tag-area__form = tag.text_field :content %span.delete-form-btn 削除する -# ↓は編集フォーム用です(データが存在する場合は削除用の非表示チェックボックスを作るため) - if @post.persisted? = tag.check_box :_destroy, data:{ index: tag.index }, class: "hidden-destroy" -# ↓フォーム追加のイベント発火用です .add-form-btn 追加する = f.submit "投稿"JSファイル
JSファイル$(function(){ function buildField(index) { // 追加するフォームのhtmlを用意 const html = `<div class="js-file-group" data-index="${index}"> <div class="tag-area__title"> タグ </div> <div class="tag-area__form"> <input type="text" name="post[tags_attributes][${index}][content]" id="post_tags_attributes_${index}_content"> <span class="delete-form-btn"> 削除する </span> </div> </div>`; return html; } let fileIndex = [1, 2, 3, 4] // 追加するフォームのインデックス番号を用意 var lastIndex = $(".js-file-group:last").data("index"); // 編集フォーム用(すでにデータがある分のインデックス番号が何か取得しておく) fileIndex.splice(0, lastIndex); // 編集フォーム用(データがある分のインデックスをfileIndexから除いておく) let fileCount = $(".hidden-destroy").length; // 編集フォーム用(データがある分のフォームの数を取得する) let displayCount = $(".js-file-group").length // 見えているフォームの数を取得する $(".hidden-destroy").hide(); // 編集フォーム用(削除用のチェックボックスを非表示にしておく) if (fileIndex.length == 0) $(".add-form-btn").css("display","none"); // 編集フォーム用(フォームが5つある場合は追加ボタンを非表示にしておく) $(".add-form-btn").on("click", function() { // 追加ボタンクリックでイベント発火 $(".tag-area").append(buildField(fileIndex[0])); // fileIndexの一番小さい数字をインデックス番号に使ってフォームを作成 fileIndex.shift(); // fileIndexの一番小さい数字を取り除く if (fileIndex.length == 0) $(".add-form-btn").css("display","none"); // フォームが5つになったら追加ボタンを非表示にする displayCount += 1; // 見えているフォームの数をカウントアップしておく }) })編集用にいくつか変数を用意してありますが、とりあえず無視してOKです。(後ほど説明します)
JSファイルの冒頭の部分でhtmlのコードを準備していますが、これはform_with, fields_forを使ったときに作成されるコードをそのまま使っています。Chromeの検証ツールを使うと見れますので見てみましょう!
こんな感じで表示されているはずです。
追加するのはjs-file-group以下なので、その部分をコピーして、js-file-groupのインデックス番号とフォームの番号の部分は引数が入るような関数を用意すればOKです。フォームの削除
次に削除の流れを確認してみます。
- 削除ボタンをクリックしてイベントを発火させ、クリックした箇所のインデックス番号を取得
- 取得したインデックス番号に応じて、①フォームの削除、②チェックボックスのチェック&フォームの非表示をする
- フォームが1つしかないときに削除ボタンを押してもフォームが消えないようにする
削除は追加に比べるとやや面倒ですが、順番に処理していけば大丈夫なので冷静に見ていきましょう。
HTMLファイル
HTMLファイルは特に変更はありません。
JSファイル
JSファイル$(function(){ // (省略) $(".tag-area").on("click", ".delete-form-btn", function() { // 削除ボタンクリックでイベント発火 $(".add-form-btn").css("display","block"); // どの道フォームは一つ消えるので、追加ボタンを必ず表示させるようにしておく const targetIndex = $(this).parent().parent().data("index") // クリックした箇所のインデックス番号を取得 const hiddenCheck = $(`input[data-index="${targetIndex}"].hidden-destroy`); // 編集用(クリックした箇所のチェックボックスを取得) var lastIndex = $(".js-file-group:last").data("index"); // フォームの最後に使われているインデックス番号を取得 displayCount -= 1; // 表示されているフォーム数を一つカウントダウン if (targetIndex < fileCount) { // 編集用(チェックボックスがある場合の処理) $(this).parent().parent().css("display","none") // フォームを非表示にする hiddenCheck.prop("checked", true); // チェックボックスにチェックを入れる } else { // チェックボックスがない場合の処理 $(this).parent().parent().remove(); // フォームを削除する } // ↓はfileIndex(フォーム追加ように用意してある数字の配列)の処理 if (fileIndex.length >= 1) { // 数字が一つでも残っている場合 fileIndex.push(fileIndex[fileIndex.length - 1] + 1); // 配列の一番右側にある数字に1を足した数字を追加 } else { fileIndex.push(lastIndex + 1); // フォームの最後の数字に1を足した数字を追加 } // ↓はフォームがなくならないための処理 if (displayCount == 0) { // 見えてるフォームの数が0になったとき $(".tag-area").append(buildField(fileIndex[0] - 1)); // fileIndexの一番左側にある数字から1引いた数字でフォームを作成 fileIndex.shift(); // fileIndexの一番小さい数字を取り除く displayCount += 1; // 見えているフォームの数をカウントアップしておく } }) })削除の場合は、フォームの処理とインデックス番号の処理が条件によって違うので注意が必要です。また、残り一つのフォームは削除されないようにしておきたいです。
順番に説明していきます。フォームの処理
単純にフォームを削除する場合
この場合、クリックした部分の親要素(今回の例ではjs-file-group)ごと削除するだけです。
編集時にデータが入っているフォームを削除する場合
この場合、フォームを削除してそのまま送信ボタンを押してもデータは更新されません。
フォームを削除するのではなく、削除用のチェックボックスにチェックをつけてフォーム自体は非表示にする必要があります。インデックス番号の処理
フォームを削除したらその分インデックス番号を増やしておく必要があります。(削除 → 追加を繰り返すとインデックス番号がなくなってしまうため)
今回インデックス番号の配列の中には数字が4つまで入るようにしておきたいです。また、すでにフォームに使われている数字と重複しないようにする必要があります。インデックス番号が一つ以上残っているとき
これはフォームが1〜4つある状態のどれかです。番号が被らないようにしたいので、配列の一番右にある数字に1を足した数字を追加すれば良いです。(例えばfileIndexが[2,3,4]の場合は5を追加する)
インデックス番号がないとき
これはフォームが5つある状態です。配列が空なのでどの数字を使えば良いか考える必要があります。フォームにそれぞれ番号が振ってあるはずなので、最後のフォームに使われている数字に1を足した数字を追加すればOKです。
フォームがなくならないための処理
フォームが全て消えてしまうと良くないので、その時の処理です。消えないようにするというよりは、全て消えたらフォームを追加するという処理になります。
見えているフォームの数を数えておいて、それが0になったら追加の処理と同様の処理をするだけです。
見えているフォームが1つの時は削除ボタンを非表示にするというのもアリかと思います。注意
削除のイベントはjsで追加した削除ボタンも使用するので、書き方には注意が必要です。
$(追加ボタンのクラス).on("click", function(){処理})
ではうまくいかないので、$("追加ボタンの親クラス").on("click", "追加ボタンのクラス", function(){処理})
としてあげましょう。追加ボタンの親クラスはjsで追加されない親クラスを書いておけば良いですが、何も考えず"document"でもOKです。編集時の考え方
編集時は削除ボタンを押してもフォームが消えないようにしたいため、条件分岐を少し工夫する必要があります。上の削除の解説を見るとわかるかもしれませんが、少し解説してみます。
クリックしたフォームにチェックボックスがあるかどうかの判定
ページを読み込んだときにチェックボックスの数を数えておきます。(fileCount)
編集ページでは、(fileCount - 1)までのインデックス番号でフォームが作成されているため、fileCountとクリックした箇所のインデックス番号(targetIndex)を比較することで、削除するか非表示にするかの判別をします。フォームがなくならないための判定
今回の例でいくとjs-file-groupの数を数えて判定しても良さそうですが、編集時はフォームを非表示にするだけで消えないフォームもあるので、この方法だとうまくいきません。
なので、ページ読み込み時のフォームの数を起点として、追加イベントでは1つカウントアップし、削除イベントでは1つカウントダウンすることで、見えているフォームの数を追うことができます。最終的なJSファイルのコード
最後にまとめて見たほうがわかりやすいと思うので載せておきます。
JSファイル$(function(){ function buildField(index) { const html = `<div class="js-file-group" data-index="${index}"> <div class="tag-area__title"> タグ </div> <div class="tag-area__form"> <input type="text" name="post[tags_attributes][${index}][content]" id="post_tags_attributes_${index}_content"> <span class="delete-form-btn"> 削除する </span> </div> </div>`; return html; } let fileIndex = [1, 2, 3, 4] var lastIndex = $(".js-file-group:last").data("index"); fileIndex.splice(0, lastIndex); let fileCount = $(".hidden-destroy").length; let displayCount = $(".js-file-group").length $(".hidden-destroy").hide(); if (fileIndex.length == 0) $(".add-form-btn").css("display","none"); $(".add-form-btn").on("click", function() { $(".tag-area").append(buildField(fileIndex[0])); fileIndex.shift(); if (fileIndex.length == 0) $(".add-form-btn").css("display","none"); displayCount += 1; }) $(".tag-area").on("click", ".delete-form-btn", function() { $(".add-form-btn").css("display","block"); const targetIndex = $(this).parent().parent().data("index") const hiddenCheck = $(`input[data-index="${targetIndex}"].hidden-destroy`); var lastIndex = $(".js-file-group:last").data("index"); displayCount -= 1; if (targetIndex < fileCount) { $(this).parent().parent().css("display","none") hiddenCheck.prop("checked", true); } else { $(this).parent().parent().remove(); } if (fileIndex.length >= 1) { fileIndex.push(fileIndex[fileIndex.length - 1] + 1); } else { fileIndex.push(lastIndex + 1); } if (displayCount == 0) { $(".tag-area").append(buildField(fileIndex[0] - 1)); fileIndex.shift(); displayCount += 1; } }) })最後に
もう少しシンプルな方法もありそうですが、とりあえずこれでうまく動作するので初めて実装する方なんかは参考にしてみてください。
もっと上手いやり方や間違っていることなどありましたらコメントいただけると幸いです。最後までありがとうございました。
- 投稿日:2020-06-24T12:32:14+09:00
finally で async ジェネレータの反復完了時、中断時、エラー発生時の全てで処理を行う
DB やファイルなどからデータをたくさん取り出して
それぞれのデータに対して何かしたい場合は多くあると思いますasync function* readLines() { console.log('ファイル開く処理') for (let i = 1; i <= 5; ++i) yield { textContent: `${i} 番目の行` } console.log('ファイル閉じる処理') } for await (const line of readLines()) { console.log(line.textContent) } // ファイル開く処理 // 1 番目の行 // 2 番目の行 // 3 番目の行 // 4 番目の行 // 5 番目の行 // ファイル閉じる処理解決すべき課題
上記の
readLines
には問題がありますasync function* readLines() { console.log('ファイル開く処理') for (let i = 1; i <= 5; ++i) yield { textContent: `${i} 番目の行` } console.log('ファイル閉じる処理') } let tmp = 0 for await (const line of readLines()) { console.log(line.textContent) if (++tmp >= 3) break } // ファイル開く処理 // 1 番目の行 // 2 番目の行 // 3 番目の行途中で
break
すると ファイル閉じる処理 が実行されませんあるいは途中でエラーが起こると…
async function* readLines() { console.log('ファイル開く処理') for (let i = 1; i <= 5; ++i) { if (3 === i) throw new Error('何かエラー') yield { textContent: `${i} 番目の行` } } console.log('ファイル閉じる処理') } for await (const line of readLines()) { console.log(line.textContent) } // ファイル開く処理 // 1 番目の行 // 2 番目の行 // Error: 何かエラーこちらでも ファイル閉じる処理 が実行されません
finally を使う
反復完了時、または中断時、そしてエラー発生時の全てで処理を行うには
finally
が使えますasync function* readLines() { console.log('ファイル開く処理') try { for (let i = 1; i <= 5; ++i) yield { textContent: `${i} 番目の行` } } finally { console.log('ファイル閉じる処理') } } let tmp = 0 for await (const line of readLines()) { console.log(line.textContent) if (++tmp >= 3) break } // ファイル開く処理 // 1 番目の行 // 2 番目の行 // 3 番目の行 // 4 番目の行 // 5 番目の行 // ファイル閉じる処理async function* readLines() { console.log('ファイル開く処理') try { for (let i = 1; i <= 5; ++i) { if (3 === i) throw new Error('何かエラー') yield { textContent: `${i} 番目の行` } } } finally { console.log('ファイル閉じる処理') } } for await (const line of readLines()) { console.log(line.textContent) } // ファイル開く処理 // 1 番目の行 // 2 番目の行 // ファイル閉じる処理 // Error: 何かエラー
- 投稿日:2020-06-24T12:26:50+09:00
Vuetifyでテキストとボタンを横に並べる
実現したいこと
Vuetifyでテキストボックスとボタンを横並びにしたい。
ちょっと実現したいデザインは違うけど、フォームの構成としては、以下の検索窓のような感じです。テキストボックスに入力後、ルーペアイコンのボタンをクリックして検索するというパターンです。
実現方法
入力欄に使用する
v-text-field
のappend
スロットを使うと実現できました。
vuetifyの公式ドキュメントでは、Icon slotsとして紹介されています。スロットの
append-outer
を使って、以下のようにフォームを作成します。<v-text-field v-model="message" label="Message" type="text" > <template v-slot:append-outer> <v-btn color="primary">検索</v-bt> </template> </v-text-field>こんな感じになります。
codepenでサンプルを作成しました。
こちらは、スロットのappend
も記述して、append-outer
と比較しています。冒頭の検索窓のように、テキストの中にボタンがあるパターンですと、
append
スロットを指定する方が合います。テキストとボタンを並べる場合は、append-outer
がよいですね。See the Pen Vuetify Example Pen by Shigehiro IDANI (@1da2) on CodePen.
まとめ
公式ドキュメントのコンポーネントを眺めていても、組み合わせ方とかは、ある程度の経験が必要になってきます。
このタイプも、知っていれば簡単なのですが、知らないと、ズバリが見つけられなくて時間がかかるパターンでした。
- 投稿日:2020-06-24T12:23:43+09:00
【JavaScript】プロパティの変化を監視する
はじめに
React.jsやVue.jsのようなJSフレームワークを使用してない案件で、オブジェクトのプロパティの変化を監視しないといけないことがあったので、生のJavaScriptで実装しました。
今時のフロントエンド開発では、あまりなさそうな気もしますが。。。サンプルプログラム
sample.js//オブジェクトのプロパティの変化を監視する関数 let watchProp = function(obj, propName) { let value = obj[propName]; Object.defineProperty(obj, propName, { get: () => value, set: newValue => { const oldValue = value; value = newValue; consoleOut(oldValue,value); } }); } //プロパティが変化した時に呼び出す関数 let consoleOut = function(oldValue,value){ document.getElementById("outTime").innerHTML = oldValue+"⇒"+value; } //監視させるオブジェクトとプロパティを定義 let timeKeeper = {"count_time" : 0}; watchProp(timeKeeper, "count_time"); //1秒ごとにプロパティをカウントアップ setInterval("countup()", 1000); let countup = function(){ timeKeeper["count_time"] += 1; console.log(timeKeeper["count_time"]); }サンプルプログラムはこんな感じで、以下のようなhtmlで読み込ませてあげると、1秒毎に<p>タグのテキストが変化します。
sample.html<!DOCTYPE html> <html> <head></head> <body> <p id="outTime">0</p> <script type="text/javascript" src="sample.js"></script> </body> </html>【ブラウザ表示】
Object.definePropertyでプロパティの変化を監視
let watchProp = function(obj, propName) { let value = obj[propName]; Object.defineProperty(obj, propName, { get: () => value, set: newValue => { const oldValue = value; value = newValue; consoleOut(oldValue,value); } }); }サンプルプログラムでは、この「watchProp」という関数がプロパティの変化を監視して、変化した時に「consoleOut」という関数を呼び出しているのですが、 Object.defineProperty() というメソッドを使用しています。
詳しくは、リンクのMDNや他の方の記事が参考になるかと思います。
Object.defineProperty() - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty「[JavaScript] 6. Object.defineProperty/ies でプロパティ/メソッド定義」
https://qiita.com/Koizumi-Greenwich/items/1b827b4304538f2f8e37
- 投稿日:2020-06-24T11:53:11+09:00
プレーンなJavaScriptを使って別ファイルのJSONの情報を引っ張ってくる方法
前提
planeなJavascriptを使って外部ファイルのJSONの情報を取得するやり方です。
JSONを久しく触っていない+いつもjQueryでやっていたのでやり方がわからず時間をとったのでここにメモします。
ファイルを作る
まずHTMLファイルとJSONファイルを作成します。
中身はお互いこんな感じ。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>json research</title> </head> <body> <div class="wrapper"> <input type="button" value="JSONファイルを取得し、内容をブラウザのコンソールへ出力" onclick="getJSON();"> </div> </body> </html>[ {"id":1,"name":"AAA"}, {"id":2,"name":"BBB"}, {"id":3,"name":"CCC"} ]イメージとしてはボタンをクリックするとJSONにある情報を取ってくるといった感じです。
Javascript
古かったら申し訳ないのですが、以下で動作確認できました。
<script> function getJSON() { let req = new XMLHttpRequest(); req.onreadystatechange = function() { // サーバーからのレスポンスが正常&通信が正常に終了したとき if(req.readyState == 4 && req.status == 200) { // 取得したJSONファイルの中身を変数へ格納 let data = JSON.parse(req.responseText); // JSONのデータ数を取得 let len = data.length; for (let i = 0; i < len; i++) { console.log("id: " + data[i].id + ", name: " + data[i].name); } } }; //HTTPメソッドとアクセスするサーバーのURLを指定 req.open("GET", "./sample.json", false); //実際にサーバーへリクエストを送信 req.send(null); } </script>まとめ
JSONは結構使う場面が多いので覚えておきましょう。
参考
- 投稿日:2020-06-24T11:41:40+09:00
<a>タグのリンクを無効化するには?
aタグにはdisable属性がない
Chromeの場合、<input> <textarea> <select>タグなどはdisabledを書けば非活性になりますが、<a>タグにはdisable属性がないので利きません。
aタグのリンクを無効化するには?
csspointer-events:none;上記で非活性化することができます。
<img>タグも同様です。ある条件のときだけ複数の項目を一括で非活性にするには?
そういう場合はJavaScriptを使います。
変更したい項目を<div>で囲み、idを付与します。タグ名で要素を指定することで、項目を複数取得することが出来ます。
【document.getElementById("ID名").getElementsByTagName("input")】それをFor文で回して各項目ごとにdisable属性や、css.Style属性を付与していきます。
JavaScriptwindow.onload = function(){ var flg = $("#modeflg").val(); /*条件*/ if(flg == "2"){ var inputItem = document.getElementById("changeDisable").getElementsByTagName("input"); for(var i=0; i<inputItem.length; i++){ inputItem[i].disabled = true; } var inputItem = document.getElementById("changeDisable").getElementsByTagName("textarea"); for(var i=0; i<inputItem.length; i++){ inputItem[i].disabled = true; } var selectItem = document.getElementById("changeDisable").getElementsByTagName("select"); for(var i=0; i<selectItem.length; i++){ selectItem[i].disabled = true; } var selectItem = document.getElementById("changeDisable").getElementsByTagName("a"); for(var i=0; i<selectItem.length; i++){ selectItem[i].style.pointerEvents = 'none'; } var selectItem = document.getElementById("changeDisable").getElementsByTagName("img"); for(var i=0; i<selectItem.length; i++){ selectItem[i].style.pointerEvents = 'none'; } } };
- 投稿日:2020-06-24T11:28:45+09:00
技術書展8にて出典していた技術本をBOOTHにて販売開始しました。(対応が遅れてしまい申し訳ありませんでした。。。)
技術書展8にて出典していた技術本「WebRTCとngrokを使用したリアルタイムビデオチャットWEBアプリの作成」
継続して販売して欲しいという要望をいただいていたのに対応が遅れてしまい申し訳ありませんでした。
本書から得られる内容を下部に記載しておきます。
https://kasata.booth.pm/items/2067296
https://kasata.booth.pm/items/2067296
【本書で得られる成果物】
本書を初めから最後まで一通り行っていただくと、
最後にはサンプルとして動作する、 多人数ビデオ通話&テキストチャットアプリが完成します。
また、そのアプリを外部の人に試してもらえる環境構築方法も掲載しております。
※本書で作成するWebアプリケーションの作成までは、全て無料の範囲で行って いただけます。
【使用技術1:WebRTC、SkyWay】
プラグインを追加することなくWebブラウザ上でリアルタイムコミュニケーションを 可能にするオープンフレームワーク、
「WebRTC(Web Real-Time Communications)」を使用した、 低遅延多人数ビデオ通話&テキストチャットサービスなどを作成することができ ます。
その際、より高速にWebRTCアプリを開発することを重視するためにNTTコミュニ ケーションズ社が提供している便利なツール「SkyWay」を使用する手順も解説します。
【使用技術2:ngrok】
昨今の実務におけるスピード重視のプロトタイプ開発の潮流を考慮し、
クラウドサービスなどへデプロイする前段階の手軽な方法として
ローカルサーバーで動いているアプリを外部の人へ公開ができるツールである ngrokの使用方法も解説します。
これはWebRTCだけでなく、Djangoなどで作成したWebアプリでも簡単に公開が可能。
【使用技術3:HEROKU】
Herokuは有名なPaaSの一つです。「PaaS」は「Platform as a Service(プラットフォーム アズ ア サービス)」の略で、
Webサービスを公開するために必要なものを全て、予め用意してくれるという サービスです。
自身で開発したアプリをサーバー周りのことを詳しく知らなくても容易に動かせるようになります。
- 投稿日:2020-06-24T06:23:23+09:00
Vuex の初級編 Vue CLI版 #Vue.js #vuex
概要
Vuex の初級編的な内容で
Vue CLI で実装例となります。・ components間で、prop等で
値を受け渡した時に。反映できない場合がありましたので
vuexに、変更してみました。
参考のコード
https://github.com/kuc-arc-f/vuex_sample_1
構成
Vue CLI
vuex : 3.4.0
vue: 2.6.11参考
https://qiita.com/okumurakengo/items/0521049e79f927632cab
vuex 追加
npm install vuex --savepackage.json
https://github.com/kuc-arc-f/vuex_sample_1/blob/master/package.json
実装など
getters, mutations を定義しています。
・store.js
https://github.com/kuc-arc-f/vuex_sample_1/blob/master/src/store.jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // export default new Vuex.Store({ state: { products: [ { id: 1, name: "Hoge", stock: 0 }, { id: 2, name: "Fuga", stock: 3 }, { id: 3, name: "Piyo", stock: 0 }, ], tasks:[], count: 5, }, getters: { depletedProducts: state => { return state.products }, getTasks: state => { return state.tasks } }, mutations: { increment(state) { state.count++ }, setTasks(state, payload) { state.tasks = payload.tasks } }, })・main.js
store を、読み込んでいますimport Vue from 'vue' import router from './router' import App from './App.vue' import store from './store' Vue.config.productionTip = false // new Vue({ store, router, render: h => h(App), }).$mount('#app')components
・読込、computed で、getters からstore.js
で、設定した。固定値を取得
・this.$store.getters.定義した取得関数で、読めるので
importなして。グローバル参照が可能でしたTestVuex.vue
https://github.com/kuc-arc-f/vuex_sample_1/blob/master/src/components/TestVuex.vueexport default { created(){ console.log( "count =" + this.$store.state.count ) }, computed: { depletedProducts() { return this.$store.getters.depletedProducts; } }, methods: { updateCount() { this.$store.commit("increment"); } } }・表示の部分
<div> <h1>TestVuex.vue</h1> Test-1: <p>Count: {{ $store.state.count }}</p> <button @click="updateCount">increment</button> <hr /> depletedProducts: <ul> <li v-for="(product, i) in depletedProducts" :key="i"> name: {{ product.name }} , ID : {{ product.id }} </li> </ul> </div>親/子 components で、読み書きする場合
・親、TestVuex_2.vue
https://github.com/kuc-arc-f/vuex_sample_1/blob/master/src/components/TestVuex_2.vuemutations で、commit( this.$store.commit('setTasks', {'tasks' : items } ) )
import {Mixin} from '../mixin' import TestChild from '../components/Element/TestChild' // export default { mixins:[Mixin], components: { TestChild }, created(){ console.log( "count =" + this.$store.state.count ) this.updateTask() // console.log( this.getTasks ) }, methods: { updateCount() { this.$store.commit("increment"); }, updateTask() { var items = [ { 'id' : 1, 'name' : 'n1'}, { 'id' : 2 , 'name' : 'n2'}, { 'id' : 3 , 'name' : 'n3'}, ] this.$store.commit('setTasks', {'tasks' : items } ) }, } }・子、TestChild.vue
https://github.com/kuc-arc-f/vuex_sample_1/blob/master/src/components/Element/TestChild.vuecomputed , getters で、読み込み ( this.$store.getters.getTasks; )
import {Mixin} from '../../mixin' // export default { mixins:[Mixin], created () { // console.log( "uid=" + this.user_id ) }, computed: { getTasks(){ return this.$store.getters.getTasks; } }, methods: { } }
- 投稿日:2020-06-24T02:25:02+09:00
React初心者が試行錯誤しながらサイト作ってみた(その1)
つまずいた部分のメモ
Material-UIはどうやってスタイル変えるの!?
パッと(これが原因で後でエラーが...)調べたところ
makeStyles
とwithStyles
が出てきた今回は
makeStyles
を使うことにしました.makeStyles が使えない事件!!
早速使って見たら以下のエラーが,,,
Header.jsconst useStyles = makeStyles({ title: { flexGrow: 1, }, }); export default class Header extends.React.Component() { render (){ const classes = useStyles();//Error return (...); } }ちゃんと公式サイトで調べたら
makeStyles
はフックAPIだということが判明! <-- ちゃんと調べろ自分
というわけで関数コンポーネントに変更したらできました.Header.jsconst useStyles = makeStyles({ title: { flexGrow: 1, }, }); export default function Header() { const classes = useStyles();//OK return (...); }
その他のスタイル変更
Header.js//style={{...: ...}}で各種変更可 <AppBar style={{ background: '#d1c4e9' }}>//AppBarの背景の色変更 <MenuIcon style={{ color: purple[800]}}>//MenuIconの色変更 //makeStylesを実際に適用 const useStyles = makeStyles({ title: { flexGrow: 1, }, }); export default function Header() { const classes = useStyles(); return ( <Typography variant="h6" className={classes.title}>//適用 Title </Typography> ); }次はTeitterとGithubのアイコン表示や,メニューボタン押したら一覧が出てくる機能を追加してみます.
- 投稿日:2020-06-24T00:15:28+09:00
luma.glへの挑戦
luma.gl
世の中には多くのWebGLフレームワークが存在します。
高度に抽象化された例では、three.jsが有名だし、PlayCanves、Babylon.js、A-frameなどのゲーム用のフレームワークもよく使われている気がします。
私が注目しているのが、deck.glというUberが出している可視化エンジンで、これのベースとなっているのが、今回注目するluma.glです。
特徴としては以下があるとのこと。
1. シンプルに抽象化されたハイパフォーマンスなデータ可視化のAPIを提供する
2. WebGL1とともに、WebGL2のAPIをサポートすることで、クロスプラットフォームにおける煩雑さを軽減するdeck.glに比べて注目度は低いように思いますが、既にこれをベースとしてエコシステムが生まれています。標準化を目指した志の高いライブラリらしいです。具体的には、以下のライブラリが良く使いそうです。
1.loaders.gl : GLTFなどの形状のインポートを行う
2.math.gl : 行列計算を行う準備
以下は、公式ページからの引用です。node.jsとwebpack等を用いて環境を整備します。
適当なフォルダを作ったら以下のコマンドを実行します。npm init -y npm i @luma.gl/engine @luma.gl/webgl npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin
package.json
に起動時のコマンドを登録します。{ "name": "luma-demo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "webpack-dev-server --open" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@luma.gl/engine": "^8.1.2", "@luma.gl/shadertools": "^8.1.2", "@luma.gl/webgl": "^8.1.2", "deck.gl": "^8.1.9" }, "devDependencies": { "html-webpack-plugin": "^4.3.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.11.0" } }続いて、webpack用の設定を行います。
webpack.config.jsconst path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { mode: 'development', entry: './index.js', plugins: [ new HtmlWebpackPlugin({ title: 'luma.gl Demo', }), ], output: { filename: 'bundle.js' }, };これで準備は終わりです。簡単な事例として、公式サイトには以下の例が記されています。特に何もせずに、画面を黒で初期化する例です。
AnimationLoop()
が、いわゆるrequestAnimationFrame()
に対応する部分です。定義されているハンドラからは、様々な経過フレーム数など多様な情報の取得が可能です。index.jsimport {AnimationLoop} from '@luma.gl/engine'; import {clear} from '@luma.gl/webgl'; const loop = new AnimationLoop({ onInitialize({gl}) { // Setup logic goes here }, onRender({gl}) { // Drawing logic goes here clear(gl, {color: [0, 0, 0, 1]}); } }); loop.start();APIのコンセプト
luma.glでは、様々なレベルの開発要求に対応できるように3つのレベルに抽象化されているようです。
- Low-Level : 直接的にWebGLのAPIを用いて、コンテキストやシェーダなどを管理する軽量な管理ツールを有する。
shadertools
、gltootls
、debug
モジュールを 主として用いる。- Mid-Level : WebGLをラップする便利なAPIを有する。
webgl
モジュールを用いる。- High-Level : モデルやリソースを管理する3Dエンジンによる開発ができる。
engine
、webgl
を用いる。いずれにせよ、頂点シェーダ、フラグメントシェーダといったWebGL、GLSLの基本知識は必須です。wgld.orgなどで勉強しましょう。
High-Levelの例
まずは
engine
モジュールを用いた、ハイレベルなAPIについてです。
普通のWebGLのプログラムをうまいこと抽象化しているイメージです。以下では頂点シェーダ
vs
とフラグメントシェーダfs
を定義して、その後にモデルを構築しています。import {AnimationLoop, Model} from '@luma.gl/engine'; import {Buffer, clear} from '@luma.gl/webgl'; const colorShaderModule = { name: 'color', vs: ` varying vec3 color_vColor; void color_setColor(vec3 color) { color_vColor = color; } `, fs: ` varying vec3 color_vColor; vec3 color_getColor() { return color_vColor; } ` }; const loop = new AnimationLoop({ onInitialize({gl}) { const positionBuffer = new Buffer(gl, new Float32Array([ -0.2, -0.2, 0.2, -0.2, 0.0, 0.2 ])); const colorBuffer = new Buffer(gl, new Float32Array([ 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0 ])); const offsetBuffer = new Buffer(gl, new Float32Array([ 0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5 ])); const model = new Model(gl, { vs: ` attribute vec2 position; attribute vec3 color; attribute vec2 offset; void main() { color_setColor(color); gl_Position = vec4(position + offset, 0.0, 1.0); } `, fs: ` void main() { gl_FragColor = vec4(color_getColor(), 1.0); } `, modules: [colorShaderModule], attributes: { position: positionBuffer, // divisor: glVertexAttribDivisorのこと color: [colorBuffer, {divisor: 1}], offset: [offsetBuffer, {divisor: 1}] }, vertexCount: 3, instanceCount: 4, instanced: true }); return {model}; }, onRender({gl, model}) { clear(gl, {color: [0, 0, 0, 1]}); model.draw(); } }); loop.start();modelの
attributes
内に記載されているキーと値が、シェーダに渡されます。
構築したmodelに対して、onRender
ハンドラ部分でdraw()
が呼ばれていまonRender'ハンドラの引数になっているのは、
onInitialize'ハンドラの返り値ですね。。
この構成が基本で、Mid-Levelの例では、Modelをより細かく設定し、シェーダを組み合わせてプログラムを作って実行する例が示されています。Low-Levelは、ほとんどそのままWebGLを書いている感じです。メッシュを描く
上の例はサンプルそのままなので、Geometryクラスを使って、メッシュを描くことをやってみます。
WebGLでいうと、drawElements
に当たるところ。ドキュメントがあまりに不親切だったので、ソースを読み解きながらの実装でした。
うまくいくと、以下のようにカラフルな四角形が回転を始めます。wgld.orgを参考にさせていただきました。import {AnimationLoop, Model, Geometry} from '@luma.gl/engine'; import {Buffer, clear} from '@luma.gl/webgl'; import {setParameters} from '@luma.gl/gltools'; import {Matrix4} from 'math.gl'; const vs = ` attribute vec3 positions; attribute vec4 color; uniform mat4 mvpMatrix; varying vec4 vColor; void main(void){ vColor = vec4(color); gl_Position = mvpMatrix * vec4(positions, 1.0); } `; const fs = ` precision mediump float; varying vec4 vColor; void main(void){ gl_FragColor = vColor; } `; // 頂点属性を格納する配列 var position = [ 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, -1.0, 0.0 ]; var color = [ 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ]; var index = [ 0, 1, 2, 1, 2, 3 ]; const loop = new AnimationLoop({ onInitialize({gl}) { setParameters(gl, { depthTest: true, depthFunc: gl.LEQUAL }); // ModelのAttributesに通す場合はBufferになる const colorBuffer = new Buffer(gl, new Float32Array(color)); const eyePosition = [0, 0, 5]; const viewMatrix = new Matrix4().lookAt({eye: eyePosition}); const mvpMatrix = new Matrix4(); const model = new Model(gl, { vs, fs, geometry: new Geometry({ attributes: { positions: new Float32Array(position), }, indices: new Uint16Array(index) }), attributes: { color: [colorBuffer] } }); return { model, viewMatrix, mvpMatrix }; }, onRender({gl, aspect, tick, model, mvpMatrix, viewMatrix}) { mvpMatrix.perspective({fov: Math.PI / 3, aspect}) .multiplyRight(viewMatrix) .rotateX(tick * 0.01) .rotateY(tick * 0.013); clear(gl, {color:[0,0,0,1], depth:1.0}); model.setUniforms({mvpMatrix: mvpMatrix}) .draw(); } }); loop.start();上記のサンプルで基本的なところは押さえられたかと思います。
参考