20200624のJavaScriptに関する記事は28件です。

攻撃から学ぶ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

  1. 攻撃者はWebサイトのフォームを使用して、データベースに悪意のあるスクリプトを挿入します。
  2. 被害者はWebサイトにページをリクエストします。
  3. Webサイトの応答には、データベースからの悪意のあるコードが含まれ、被害者に送信されます。
  4. 被害者のブラウザ内で、悪意のあるスクリプトを実行され、被害者の情報が攻撃者のサーバーに送信されます。

Reflected XSS

  1. 攻撃者は悪意のあるスクリプトを含むURLを作成して被害者に送信します。
  2. 被害者は、悪意のあるスクリプトを含むURLのページをリクエストします。
  3. レスポンスにはURLに含まれる悪意のあるスクリプトが含まれています。
  4. 被害者のブラウザ内で、悪意のあるスクリプトを実行され、被害者の情報が攻撃者のサーバーに送信されます。

DOM-based XSS

  1. 攻撃者は悪意のあるスクリプトを含むURLを作成して被害者に送信します。
  2. 被害者は、悪意のあるスクリプトを含むURLのページをリクエストします。
  3. レスポンスにはURLに含まれる悪意のあるスクリプトが含まれていません。
  4. 応答された正当なDOMをレンダリングした後に、悪意のあるスクリプトがページに挿入されます。
  5. 被害者のブラウザ内で、挿入された悪意のあるスクリプトを実行され、被害者の情報が攻撃者のサーバーに送信されます。

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

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

【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('');
    }
  })

以上で終了です。
ご覧いただきありがとうございました。

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

GitHubの草を草色に戻すChrome拡張作った

GitHubのデザインが色々変わって、草が草の色じゃなくなったのが寂しかったので、草色にするChrome拡張を作りました。

効果

before(新デザイン)

スクリーンショット 2020-06-24 22.28.56.png

after(Chrome拡張を実行)

スクリーンショット 2020-06-24 22.29.03.png

どうですか?草ですね。落ち着きますね。

入手方法

まだChromeExtensionのストアに申請出していないので(出しても承認に数日かかる)、ローカルでビルドしてください。

リポジトリはこちらです。

ビルド方法

READMEに書きましたが、

cd extension
npm install
npm build

して、手動で拡張機能をインストールしてください。

実行方法

草のページを開いたら、ツールバーの草アイコン(↓のやつ)をクリックしてください。
icon-48.png

本当はページ開いたら自動で実行されるようにしたいですが、未対応です。

実装の解説

草本体

GitHubの草はSVGで実装されています。SVGはHTMLのDOMと同じように、JavaScriptでごりごり変更する事が可能です。

図で示しているのは、1個の草(1日の草?)です。ぱっと見、fillで入っている値が、新しい値になったのかと思いきや、この値は昔のスタイル(草色)のスタイルのままです。
scss(要はcss)で旧スタイルの色から、新スタイルの色に変換するclassを適用する事で、結構無理やり色を変えています。
daisuke-fukuda.png

なので、色を変更する処理としては、

  // 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ってなってたのですが、そういう意味あるんですかね??

スクリーンショット 2020-06-24 22.52.23.png

で、ここはSVGではなくて、ulとliで実装されています。

styleとして直書きされている方には昔の色がそのまま入っていて、scss(css)で!important で色を上書きしています。
優先度で言うと、
style(!important無し) < css(!import付き)
だからです。

daisuke-fukuda2.png

なので、色を変更するには

  // 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がどうやって今回の見た目変更を実装したかが確認できたのも良かったです。きっと色々大変なんでしょうね、、

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

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.js
function 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個書くことで生成。
碁石は、奇数手、偶数手で塗りつぶされる色を変更して円で表現。
石取りの判定が入っていないので、囲碁ではないですね。
五目並べには使えるかな(笑)

後書き

ポートフォリオの題材に囲碁アプリを作りたかったんですが、棋譜再生方法、棋譜検討機能の実装で詰まってしまいました。
碁盤・碁石の描画だけはできるようになったので、アウトプットとして投稿します。

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

jqueryでsubmitボタンを毎回有効にする方法

テックキャンプでJavaScriptの学習中に学んだ事

jQueryを使って非同期通信の学習をしている最後に、送信ボタンを押してイベント発火後に
送信ボタンが無効化されているのを有効にする方法を記します。

$('#hoge').prop('disabled', false);

調べてみてわかった事は

  • Railsのver5.0以降はdisabledがデフォルトで設定されている事(連打防止等の為)
  • 他にも有効に出来る方法はある

例えば

$('#hoge').attr('disabled', false);

自分のコードで試したが、どちらでも有効でした

今度は連打防止などの方法も調べて使え様にしていきます

非同期通信が自由に扱える様になると、少ないビューファイルの中に沢山の動的要素を
取り入れる事が出来て、かつレスポンスも早そうなので、プログラミングを学びたての自分でも
魅力的だなって感じました。

難しいけど面白い

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

javascript 神経衰弱ゲーム(自分なりの解説)

今回も、書籍ゲームで学ぶJavascript入門で紹介されている
神経衰弱ゲームの知識定着のために自分なりに解説をしてみます。

シャッフル処理

書籍の中では、
まず配列のprototypeの中に、
配列をシャッフルをする処理を記述してあります。(書籍と記述が異なります。)

shuffle.js
Array.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.js
var 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.js
startTime = 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.js
funciont 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.js
else{
src.className = "card back"; 2枚目を裏に
src.textContent ="";
prevCard.className = "card back";
prevCard.textContent ="";
prevCard = null;初期化
}

プログラムは瞬時に働くので、これだと瞬時に表に戻ってしまいます。
前引いた番号を自分で覚えるのが神経衰弱の面白さなので、1秒間待ってから消す作業に移ります。

flip.js
setTimeout(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.js
flipTimer =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.js
function flip(e){
var src =e.srcElement;
//fliptimerが値があるのは、2枚開いてる間 or すでに表になってるカードを押してる
if(flipTime ||src.textContent !=""){ 

   return; //何もしない
}

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

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)
}
関数名()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.

newcastle vs aston villa.jpg

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 App

The 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; Joelinton

Aston Villa: Nyland; Konsa, Hause, Mings, Targett; Douglas Luiz, Grealish, Hourihane; El Ghazi, Samatta, Trezeguet

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

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 City vs Everton.jpg

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 App

The 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.

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

作って理解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-fix1

break/continueのための仕組み

前回、「if/while/forはジャンプする量を事前に計算できるがbreakはできない1、ほかの仕組みを作る必要がある」と書きましたが、その仕組みを実装しました。仕組みは以下のようになります。

  • breakやcontinueを行える制御構造の開始時に「breakされたらここに飛ぶ、continueされたらここに飛ぶ」という情報をスタックに積む。このスタックをbreakableStackと呼ぶ2
  • breakやcontinueが起きたらbreakableStackから情報を取り出して指定された位置にジャンプする
  • breakやcontinueされることなく本文(forなどのStatement部分)を終えたらbreakableStackを一つ下ろす

forはかなりややこしいのでwhileで説明します。ステップ7と比べてPUSH_BREAKABLEPOP_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の引数であるbreakOffsetcontinueOffsetは名前の通り「breakされたときの飛び先」「continueされたときの飛び先」です。またoffsetという名前が示す通りこの時点では「PUSH_BREAKABLE命令がある位置からの相対位置」です。

VMではPUSH_BREAKABLEを見つけると絶対位置を計算したうえでbreakableStackにpushします3continueOffsetで条件分岐しているのは「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

  1. forの1つ目(初期化)の命令列
  2. forの2つ目(条件)の命令列
  3. 条件を否定し、否定したものがtrueならループ末尾へ
  4. ループ本体の命令列
  5. forの3つ目(更新処理)の命令列
  6. 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
まず各部分のアセンブルを行います。

  1. switchの式をアセンブル
  2. 各caseについて式部分(Expression)を別命令配列にアセンブルしておく
  3. 同様に本文部分(StatementList)を別命令配列にアセンブルしておく

次に各部分の命令数を使って「条件にマッチした場合にどれだけジャンプすればいいか」のオフセットを計算します。
言葉だけでは絶対伝わらないので図解すると以下のようになります。ちなみに「case 1の本文」とかからジャンプがないのは間違いではありません。breakは「本文内のこと」なのでswitch自体に「本文実行したから末尾にジャンプ」という機能はありません。5

assemble_switch.png

assembler.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.js
    switch (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.js
for (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.js
import 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を作るべきか?ということも検討中です。


  1. ちなみにCRubyやCPythonはbreakに対して一旦ブロック末尾に対応するラベルに飛ぶようにしておき、後からラベルとbreakが書いてある位置の距離を計算してたはずです(末尾の位置はまだ決まってないのでbreakを見つけた時点では計算できない)。ラベル絶対使いたくないマンというわけではないですがJOKEではこの方法は採用しませんでした。 

  2. ちなみに仕様ではwhile/forなどのIterationStatementとswitch(SwitchStatement)はBreakableStatementとしてまとめられています。 

  3. このようにしているのはPUSH_BREAKABLEを出力する時点では相対位置しかわからない(whileのstmtなどは別の命令列配列に書き込ませた後マージしている)ためですが書いててやはりラベル埋め込み後から計算法の方が楽かもとも思ってきました(笑) 

  4. else ifについてはJavaScriptでは「else節の文」として再帰処理されるためシンプルです。 

  5. これにより無数のバグが生まれたのはまた別のお話。 

  6. 意味があるかは別として。ふと思いついてしまったのでテストしました。 

  7. insnsは関数単位になります。詳しくは開発記その4をご覧ください。 

  8. すでにコミットしているので、セルフホスティング完了まで延々とテストが失敗します(笑) 

  9. 例外処理はクラスを作るまでできない、クラスは実質式ですし、for-ofは配列と合わせて実装するつもりです。 

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

【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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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として走ります。
preflight_correct.png

解決方法

 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")
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

任意の深さのオブジェクトからパスを生成するコード

メモ

入力

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)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミングサークルでの活動記録

この記事について

コンピュータ研究会セキュリティ部門でやったことを、まとめながらアウトプットのために書きます。
毎週金曜日にどんどん追加していきます。

6月19日

活動内容:
https://google-gruyere.appspot.com/ のpart3, part4 までやりました。

課題:Path Traversal Attack を体験できるウェブアプリケーションの作成

ディレクトリ構成
.
├── templates
│   └── index.html
├── a.txt
├── b.txt
├── c.txt
├── pass.txt
└── app.py
index.html
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>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=&ltscript&gtalert("Your PC was broken!!")&lt/script&gt</p>
  <P>alertができるということは、実際に悪影響を及ぼすスクリプトを実行させられてしまいます。</P>
  </html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

現代のフロントエンド開発シリーズ(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です。 consolelogはどちらも識別子(identifier)で、 hello worldはLiteralです。

この構文ツリーを使用すると、コードに対してさまざまな複雑な操作を実行できます。

const { name, age } = person; // es6

var name = person.name; // es5
var age = person.age; // es5

es6の構文を古いブラウザで正しく機能させるために、最初に「babel」を導入してJavascriptをコンパイルし、ブラウザーを対応できるコードに変換します。

これは不思議に聞こえますね
「Javascriptでパーサーを作成し、JavascriptをJavascriptに変換する?」

実際、これはJavaScriptのせいにすることはできません。
ユーザーはブラウザーでWebページを表示しています。もちろん、ユーザーに強制的に更新を強制したり、必ず「Chrome使って」みたいな要求をしたりすることはできません。
バックエンドサーバーとは異なり、アップグレードしたい場合は、そのままアップグレードすればいいです。

抽象構文ツリーを作成すると、コードを簡単に操作できます。たとえば、ブラウザをサポートするには、すべてのタイプの VariableDeclarationvarに変更する必要があります。ツリー内のすべての VariableDeclarationを探して、 kindvarに変更するだけです。

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を使って、よりきれいなコードがかけるので、ぜひ理解してみましょう!

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

GoogleChatのグループチャットのスレッド一覧リンクを表示するブックマークレット(コピペでOK)

sample.png

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で最初の子要素を見つけています。

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

【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>

同シリーズ

参考文献

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

Openbase.ioって言うサイトが面白い!

この記事は何ですか?

openbase.ioっていうサイトをたまたま見つけて、面白かったのでそのサイトの紹介記事です!
注意として、openbase.ioは出来て間もないサイトなので、それを踏まえた上でこの記事を読んで欲しいです?

因みに、筆者はopenbase.ioと何の関係も無いのです。
なので、この記事はあくまで openbase.io に魅せられたユーザーの個人的な記事です。

Openbase.ioってどんなサイト?

JavaScriptのモジュールを検索したり、比較したりなどして、モジュール選択をサポートしてくれるサイトです!
以下の画像は、openbase.ioのホーム画面のスクショですが、凄くデザインが良いですよね。

Openbaseホーム画面スクショ

個人的に好きなポイント

個人的に好きなポイントを列挙していきます。

モジュールの情報が一目でわかる!

以下の画像は、React.jsopenbase.ioで表示したときの画像で、React.jsの情報を一目で分かるように表示してくれています。
ここで嬉しいのが、issuesPull Requestなどのリポジトリ情報も表示してくれます。これのおかげで、そのモジュールがちゃんとメンテナンスされているかとか分かって、凄く良いなと思ってます。

Openbase.io React詳細ページ

あと、グラフなどを用いて分かりやすくしてくれているので、モジュールのリポジトリをそのまま見るより、凄く理解しやすいです。

モジュールを比較しながら探せる!

これ結構嬉しいですよね。サービスでの技術選択でどのフレームワークを使おうかと悩んでいる時とかに、こうやって比較してくれると、選定がやりやすくなると思います。

これと同じような比較サイトとしてnpm trendsなどがありますが、情報が少なくて、ちょっと物足り無い感じがしますし、openbase.ioは前述した通り、リポジトリ情報とかも見ながら比較できるので、情報量の多いopenbase.ioが使いやすいなと個人的に思ってます。

Openbase モジュール比較ページ

openbase.ioの方で、モジュールをカテゴライズしてくれているので、そこら辺も他のサイトよりモジュールを比較しやすい要因だと思います。

以下の画像は、MVC Frameworkのカテゴリーページです。

category-ss-002.png

やっぱり、Vue.jsは強いですね?

チュートリアル動画などを見つけることが出来る!

これ、はじめ見たときは感動しました。モジュールのチュートリアル動画とか結構探すの面倒くさいですよね。
チュートリアル動画を出しているようなモジュールは少ないと思いますが、有名なモジュールには大抵あると思うので、そこらあたりを動画を見ながら比較できる点はすごく良いと思います。

以下は、React.jsのチュートリアル動画のページ

Openbase React.jsチュートリアル動画一覧ページ

依存関係を分かりやすく表示

これは以下の画像を見てもらえば分かると思います。

op-dependency-ss.png

依存関係とか調べるのめっちゃ怠いのですが、openbase.ioなら全然苦じゃないですね。
むしろ、私は楽しくてめっちゃ見てました。このサイトで一時間くらい余裕で潰せる?

あとがき

とりあえず、このくらいでopenbase.ioの紹介は終わろうと思います。
あまり、語りすぎるとよくないですからね。はぃ。
経験豊富なエンジニアの皆さんは、話が長過ぎて部下に嫌われたりしてるので、要注意ですよ?

また、UIなども綺麗にかっこよくデザインされていて、個人的に凄く好きなデザインでした。デザインの勉強とかするのに、参考にしてみもいいかもしれません。
最後に、この記事で紹介してない魅力的な所もいっぱいあるので、是非ご自身の目で確かめてください。
見るだけでも十分楽しいですよ。タブン?

ここまで読んでくれてありがとうございます。
それでは、また?

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

【Rails】JavaScriptでフォームを追加する方法

はじめに

以前、単一ページのフォームで複数のテーブルにデータを保存する方法を書きましたが、その続きとしてJavaScriptを利用してフォームを追加・削除できるようにする方法を書いていきます。
以前の記事 → https://qiita.com/koki_73/items/bc4ca80ab43e84d9704f

完成イメージは以下のような感じです。
今回はフォームを最大で5つまで作成できるようにしていきます。
de071ce4d6374ca709d2d1c9f7e9703d.gif
9b3cdc56affd1d78577359374cc13989.gif
なお、railsのバージョンは5.2.3で、ビューファイルはhamlとJqueryを使います。

概要

追加・削除の概要を簡単に説明しておきます。

  • フォームに識別用の番号を振る
  • 追加するフォームに渡す番号を配列で用意する
  • 追加のときは配列の数字を使って新しい識別番号のフォーム作成し、削除のときは配列に数字を追加する。

では追加、削除のやり方をそれぞれ説明します。

フォームの追加

まずはどんな流れでフォームを追加すればよいか確認してみます。

  1. 追加するフォームのまとまりにクラスとインデックス番号を設定する(HTMLファイル)
  2. 追加ボタンをクリックしたときにイベントを発火させ、新しいインデックス番号を使って新しいフォームを追加する。(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の検証ツールを使うと見れますので見てみましょう!
a6ab053bf000c5fc0f17909cf8c75bd0.png
こんな感じで表示されているはずです。
追加するのはjs-file-group以下なので、その部分をコピーして、js-file-groupのインデックス番号とフォームの番号の部分は引数が入るような関数を用意すればOKです。

フォームの削除

次に削除の流れを確認してみます。

  1. 削除ボタンをクリックしてイベントを発火させ、クリックした箇所のインデックス番号を取得
  2. 取得したインデックス番号に応じて、①フォームの削除、②チェックボックスのチェック&フォームの非表示をする
  3. フォームが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;
    } 
  })
})

最後に

もう少しシンプルな方法もありそうですが、とりあえずこれでうまく動作するので初めて実装する方なんかは参考にしてみてください。
もっと上手いやり方や間違っていることなどありましたらコメントいただけると幸いです。

最後までありがとうございました。

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

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: 何かエラー
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vuetifyでテキストとボタンを横に並べる

実現したいこと

Vuetifyでテキストボックスとボタンを横並びにしたい。
ちょっと実現したいデザインは違うけど、フォームの構成としては、以下の検索窓のような感じです。

テキストボックスに入力後、ルーペアイコンのボタンをクリックして検索するというパターンです。

image.png

実現方法

入力欄に使用するv-text-fieldappendスロットを使うと実現できました。
vuetifyの公式ドキュメントでは、Icon slotsとして紹介されています。

image.png

スロットの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>

こんな感じになります。

image.png

codepenでサンプルを作成しました。
こちらは、スロットのappendも記述して、append-outerと比較しています。

冒頭の検索窓のように、テキストの中にボタンがあるパターンですと、appendスロットを指定する方が合います。テキストとボタンを並べる場合は、append-outerがよいですね。

See the Pen Vuetify Example Pen by Shigehiro IDANI (@1da2) on CodePen.

まとめ

公式ドキュメントのコンポーネントを眺めていても、組み合わせ方とかは、ある程度の経験が必要になってきます。

このタイプも、知っていれば簡単なのですが、知らないと、ズバリが見つけられなくて時間がかかるパターンでした。

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

【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>

【ブラウザ表示】

timestamp.gif

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

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

プレーンな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は結構使う場面が多いので覚えておきましょう。

参考

https://www.koreyome.com/web/json-data-get/

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

<a>タグのリンクを無効化するには?

aタグにはdisable属性がない

Chromeの場合、<input> <textarea> <select>タグなどはdisabledを書けば非活性になりますが、<a>タグにはdisable属性がないので利きません。

aタグのリンクを無効化するには?

css
 pointer-events:none;

上記で非活性化することができます。
<img>タグも同様です。

ある条件のときだけ複数の項目を一括で非活性にするには?

そういう場合はJavaScriptを使います。
変更したい項目を<div>で囲み、idを付与します。

タグ名で要素を指定することで、項目を複数取得することが出来ます。
【document.getElementById("ID名").getElementsByTagName("input")

それをFor文で回して各項目ごとにdisable属性や、css.Style属性を付与していきます。

JavaScript
window.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';
        }
    }
};

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

技術書展8にて出典していた技術本をBOOTHにて販売開始しました。(対応が遅れてしまい申し訳ありませんでした。。。)

技術書展8にて出典していた技術本「WebRTCとngrokを使用したリアルタイムビデオチャットWEBアプリの作成」 

 

BOOTHにて販売開始しました!

 

継続して販売して欲しいという要望をいただいていたのに対応が遅れてしまい申し訳ありませんでした。

 

本書から得られる内容を下部に記載しておきます。

 

 https://kasata.booth.pm/items/2067296f:id:kassa877:20200308213758p:plain

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サービスを公開するために必要なものを全て、予め用意してくれるという サービスです。


自身で開発したアプリをサーバー周りのことを詳しく知らなくても容易に動かせるようになります。





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

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://vuex.vuejs.org/ja/

https://qiita.com/okumurakengo/items/0521049e79f927632cab

vuex 追加

npm install vuex --save

package.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.js

import 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.vue

export 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.vue

mutations で、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.vue

computed , 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: {
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React初心者が試行錯誤しながらサイト作ってみた(その1)

つまずいた部分のメモ

Material-UIはどうやってスタイル変えるの!?

パッと(これが原因で後でエラーが...)調べたところmakeStyleswithStylesが出てきた

今回はmakeStylesを使うことにしました.

makeStyles が使えない事件!!

早速使って見たら以下のエラーが,,,

Header.js
const useStyles = makeStyles({
  title: {
    flexGrow: 1,
  },
});
export default class Header extends.React.Component() {
  render (){
    const classes = useStyles();//Error
    return (...);
  }
}

ちゃんと公式サイトで調べたらmakeStylesはフックAPIだということが判明! <-- ちゃんと調べろ自分
というわけで関数コンポーネントに変更したらできました.

Header.js
const 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のアイコン表示や,メニューボタン押したら一覧が出てくる機能を追加してみます.

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

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.js
const 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.js
import {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つのレベルに抽象化されているようです。

  1. Low-Level : 直接的にWebGLのAPIを用いて、コンテキストやシェーダなどを管理する軽量な管理ツールを有する。shadertoolsgltootlsdebugモジュールを 主として用いる。
  2. Mid-Level : WebGLをラップする便利なAPIを有する。webglモジュールを用いる。
  3. High-Level : モデルやリソースを管理する3Dエンジンによる開発ができる。enginewebglを用いる。

いずれにせよ、頂点シェーダ、フラグメントシェーダといった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を参考にさせていただきました。

image.png

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();

上記のサンプルで基本的なところは押さえられたかと思います。

参考

  1. luma.gl : 公式サイト
  2. wgld.org : WebGLについて総合的な説明があります。とても勉強になる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む