20191201のJavaScriptに関する記事は30件です。

視覚障がい者のためにJavaScriptで、スライドショーにalt要素を実装する

JavaScript Advent Calendar9日目。

この記事のきっかけ

私は、障がい者向けに多目的トイレの情報を提供するウェブサイトを作成しています。あるとき、デモを体験していただこうとその地域の障がいをお持ちの方に集まっていただき、デモ体験会を開いたところ、視覚に障害をお持ちの方から次のようなご指摘をいただきました。

「多目的トイレの情報をスライドショーで見せるようにしてるみたいだけど、スマホのスクリーンリーダーでその写真の情報が読みとれない」

作っていた私もうかつだった、視覚に障がいがある方がパソコンやスマホでスクリーンリーダーを使っていたことは知ってはいたものの自分が取り組んでるプロジェクトのウエブサイトで、どのように読み込まれるかは想定してなく、まさに盲点を突かれました。
早速、この事態に対応しようと取り組んだ話をここでまとめてます。

まず考えて見たこと

視覚障がい者が普段、どのようにパソコンやスマートフォンを使用しているのか調べて見ました。一般にそれらのIT機器には、画面に表示されている文字や画像の場合は、alt要素に書き込まれた情報を読んでくれるスクリーンリーダーという機能があるようです。それで私は比較的導入しやすいアップルのVoiceOverで試してみることにしました。

alt要素を付けたいのに

さて、画像にalt要素を付ければスクリーンリーダーが読んでもらえることは分かりましたので、次alt要素を付けていく方法を調べて見ましたが、どこを見てもHTMLに関するページに遭遇します。いや、スライドショーはJavaScriptで配列を持ってDOMを生成したいと考えていましたので欲しい情報にたどり着けませんでした。
それで、ここで一旦停まり自分が書いたスライドショーのJavaScriptのコードを見てみました。

イメージ図
IMG_3910.jpg

html
<html>
    <body>
        <img id="sanplePrev" src="icon/left.png">
            <img id="sample_target">
        <img id="sampleNext" src="icon/right.png">
    </body>
</html>
javaScript
'use strict';

{
    const sampleImages = [
        'img/pic/images/sample/sample1.png',
        'img/pic/images/sample/sample2.png',
        'img/pic/images/sample/sample3.png',
        'img/pic/images/sample/sample4.png',
        'img/pic/images/sample/sample5.png',
        ];

    const sample_target = document.getElementById('sample_target'); 
    const samplePrev = document.getElementById('samplePrev');
    const sampleNext = document.getElementById('sampleNext');

    let currentNum = 0;

    function setMainImage(sampleImages) {
        sample_target.src = sampleImages;
    }

    setMainImage(sampleImages[currentNum])

    sampleNext.addEventListener('click', () => {
        currentNum++;
        if (currentNum === sampleImages.length) {
            currentNum = 0;
        }
        setMainImage(sampleImages[currentNum]);
    });

    samplePrev.addEventListener('click', () => {
        currentNum--;
        if (currentNum < 0) {
            currentNum = sampleImages.length -1;
        }
        setMainImage(sampleImages[currentNum]);
    });
}

ふと気づけたこと

コードを見ていた次のfunctionに目がとまりました。

function setMainImage(sampleImages) {
        sample_target.src = sampleImages;
    }

JavaScriptで、スライドショーを実装するとは言え、要はJavaScriptで、動的にDOMを生成しているのであって、ここでsrc属性を配列から持ってきていることに気づきました。つまり同じように配列でalt要素を準備すれば、このfunctionでalt属性を配列から持ってくることができるだろうと仮説を立て上記のJavaScriptのコードを下記のように変更してみました。

javaScript
'use strict';

{
    const sanpleImages = [
        {img:'img/pic/image/sanple/sanple1.png', alt:'サンプル画像1です。'},
        {img:'img/pic/image/sanple/sanple2.png', alt:'サンプル画像2です。'},
        {img:'img/pic/image/sanple/sanple3.png', alt:'サンプル画像3です。'},
        {img:'img/pic/image/sanple/sanple4.png', alt:'サンプル画像4です。'},
        {img:'img/pic/image/sanple/sanple5.png', alt:'サンプル画像5です。'},        
        ];
    const sanple_target = document.getElementById('sanple_target'); 
    const sanplePrev = document.getElementById('sanplePrev');
    const sanpleNext = document.getElementById('sanpleNext');

    let currentNum = 0;

    function setMainImage(sanpleImages) {
        sanple_target.src = sanpleImages.img;
        sanple_target.alt = sanpleImages.alt;
    }

    setMainImage(sanpleImages[currentNum])

    sanpleNext.addEventListener('click', () => {
        currentNum++;
        if (currentNum === sanpleImages.length) {
            currentNum = 0;
        }
        setMainImage(sanpleImages[currentNum]);
    });

    sanplePrev.addEventListener('click', () => {
        currentNum--;
        if (currentNum < 0) {
            currentNum = sanpleImages.length -1;
        }
        setMainImage(sanpleImages[currentNum]);
    });

}

変更点の解説

変更を加えたところは2点あります。
まず配列

 const sanpleImages = [
        {img:'img/pic/image/sanple/sanple1.png', alt:'サンプル画像1です。'},
        {img:'img/pic/image/sanple/sanple2.png', alt:'サンプル画像2です。'},
        {img:'img/pic/image/sanple/sanple3.png', alt:'サンプル画像3です。'},
        {img:'img/pic/image/sanple/sanple4.png', alt:'サンプル画像4です。'},
        {img:'img/pic/image/sanple/sanple5.png', alt:'サンプル画像5です。'},        
        ];

ここで配列をイメージファイルのパス用のimgとalt要素に付けたい説明をaltに名前を付けておきました。

次に先程のfunctionをこの付けた名前を使って配列から持ってくるようにしました。

    function setMainImage(sanpleImages) {
        sanple_target.src = sanpleImages.img;
        sanple_target.alt = sanpleImages.alt;
    }

これで、スライドショーの画像もスクリーンリーダーによって、alt要素が読み込まれることになります。

おわりに

実は、こうしてスクリーンリーダーで、スライドショーの画像の情報が読み込まれるようにしたのですが、視覚障がい者の方のためにUIを再度検討したところ、「スライドショー」の切り替えるボタンまでどう誘導するのかという問題に遭遇しました。それで、結局の所自分のプロジェクトでは、この方法は採用していません。しかし大切なことは、どんな人でも使いやすいWebサービスを提供することだと思います。もちろん各障がいのある方の事情を完璧には反映できませんが、これからもそうした方々に寄り添いながらこうしたサービスが作られることを深く願います。

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

jExcel Spreadsheetを使ってみる

jExcel Spreadsheetを試したので投稿します。

image.png

Data GridのライブラリでExcel風編集ができるものとして、Handsontableが有名ですが、商用利用の場合は有償になりました。jExcel SpreadsheetはMITライセンスなので、無償で利用する場合はHandsontableの代替手段として使えるのではないかと思います。

1. 利用方法

(1). ダウンロード

Git HUBからファイルをダウンロードして適切な場所に配置します。

(2). 配置例

任意ですが、私は以下のような感じで配置します。

static
  css
    jexcel.css
  js
    jexcel.js
index.html

(3). HTMLからの参照例。

  <link rel="stylesheet" href="https://bossanova.uk/jsuites/v2/jsuites.css" type="text/css" />
  <link rel="stylesheet" href="./static/css/jexcel.css" type="text/css"  media="all"/>
  <script src="./static/js/jexcel.js"></script>    
  <script src="https://bossanova.uk/jsuites/v2/jsuites.js"></script>

(4). 操作

・行追加

最終行のセル上で「Enter」を入力すると、新規行が追加されます。わざわざ「新規行」などのボタンを実装する必要がなくていいです。

image.png

・cellのコピー

cell右下の黒点をマウスでつまんで、下にスライドすると、値がコピーされます。Excelと同じ動きをします。

image.png

・コンテキストメニュー

セルを右クリックするとコンテキストメニューが表示されます。「行挿入」「行削除」「コピー」「貼付」「保存」などの機能があります。

image.png

2. Getting started

では、Getting startedから使いそうなものを見てみます。

(1). Loading from a javascript array

他のGridライブラリと同様。簡単に使えます。Javascriptの配列をロードする場合。data変数はJSONデータでもOK。

<div id='my-spreadsheet'></div>

<script>
data = [
    ['Mazda', 2001, 2000],
    ['Pegeout', 2010, 5000],
    ['Honda Fit', 2009, 3000],
    ['Honda CRV', 2010, 6000],
];
jexcel(document.getElementById('my-spreadsheet'), {
    data:data,
    columns:[
        { title:'Model', width:300 },
        { title:'Price', width:80 },
        { title:'Model', width:100 }
    ]
});
</script>

(2). Loading from a JSON file

サーバサイドのJSONファイルをロードする場合は、urlパラメータにファイルのパスをセットします。

<div id='my-spreadsheet'></div>
<script>
jexcel(document.getElementById('my-spreadsheet'), {
    url:'data.json',
    columns:[
        { title:'Model', width:300 },
        { title:'Price', width:80 },
        { title:'Model', width:100 }
    ]
});
</script>

(3). Loading from a CSV file

csvパラメータにファイルのパスをセットします。ヘッダレコードがあれば、「csvHeaders:true」とします。

<div id='my-spreadsheet'></div>

<script>
jexcel(document.getElementById('my-spreadsheet'), {
    csv:'demo.csv',
    csvHeaders:true,
    columns:[
        { width:300 },
        { width:80 },
        { width:100 }
    ]
});
</script>

(4). Nested headers

よく使うヘッダグループの機能もあります。

<script>
jexcel(document.getElementById('spreadsheet'), {
    data:data,
    columns: [
        { type: 'autocomplete', title:'Country', width:'300', url:'/jexcel/countries' },
        { type: 'dropdown', title:'Food', width:'150', source:['Apples','Bananas','Carrots','Oranges','Cheese'] },
        { type: 'checkbox', title:'Stock', width:'100' },
    ],
    nestedHeaders:[
        [
            { title:'Supermarket information', colspan:'3' },
        ],
        [
            { title:'Location', colspan:'1' },
            { title:' Other Information', colspan:'2' }
        ],
    ],
});

(5). Column types

列の型も必要なものは一通りそろってます。

jexcel(document.getElementById('myTable'), {
    data:data,
    columns: [
        { title:'Model', width:300, type:'text'; },
        { title:'Price', width:80, type:'numeric' },
        { title:'Date', width:100, type:'calendar', options: { format:'DD/MM/YYYY' } },
        { title:'Photo', width:150, type:'image' },
        { title:'Condition', width:150, type:'dropdown', source:['New','Used'] },
        { title:'Color', width:80, type:'color' },
        { title:'Available', width:80, type:'checkbox' },
    ]
});

(6). Calendar type

列の型が「calendar」の場合は、optionsパラメータをセットします。

options : {
    // Date format
    format:'DD/MM/YYYY',
    // Allow keyboard date entry
    readonly:0,
    // Today is default
    today:0,
    // Show timepicker
    time:0,
    // Show the reset button
    resetButton:true,
    // Placeholder
    placeholder:'',
    // Translations can be done here
    months:['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
    weekdays:['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
    weekdays_short:['S', 'M', 'T', 'W', 'T', 'F', 'S'],
    // Value
    value:null,
    // Events
    onclose:null,
    onchange:null,
    // Fullscreen (this is automatic set for screensize < 800)
    fullscreen:false,
};

(7). Dropdown and autocomplete type

Dropdown and autocompleteにも対応しています。

data = [
    ['Honda', 1, 'Civic', '4'],
    ['Peugeot', 3,'1007', '2'],
    ['Smart', 3,'Cabrio', '4;5'],
];

$('#my').jexcel({
jexcel(document.getElementById('spreadsheet'), {
    data:data,
    columns: [
        {
            type:'dropdown',
            title:'Region',
            source:['South East','South West','North','London'],
            width:'200',
        },
        {
            type:'dropdown',
            title:'Available in',
            multiple:true,
            source:[{id:1, name:'Red'},{id:2, name:'Yellow'},{id:3,name:'Blue'}],
            width:'200',
        },
        {
            type:'autocomplete',
            title:'Region',
            url:'values.json',
            width:'200',
        },
    ]
});

3. Examples

Exampleも少し見てみます。

(1). Implementation

React, VUE, Jqueryからの利用方法もあります。

(2). download

gridのデータをcsv形式でダウンロードできます。

<div id="spreadsheet1"></div>

<p><button id='download'>Export my spreadsheet as CSV</button></p>

<script>
mySpreadsheet = jexcel(document.getElementById('spreadsheet1'), {
    csv:'/jexcel/arts.csv',
    csvHeaders:true,
    tableOverflow:true,
    columns: [
        { type:'text', width:300 },
        { type:'text', width:80 },
        { type:'dropdown', width:120, source:['England','Wales','Northern Ireland','Scotland'] },
        { type:'text', width:120 },
        { type:'text', width:120 },
     ]
});

document.getElementById('download').onclick = function () {
    mySpreadsheet.download();
}

(3) Readonly columns

columnsのreadonlyをtrueにセットするだけです。
割愛しますが特定のcellだけをreadOnlyにすることもできます。

jexcel(document.getElementById('spreadsheet'), {
    data: data,
    columns: [
        {
            type: 'text',
            title:'Description',
            width:'200px',
            readOnly:true,
        },
        {
            type: 'text',
            title:'Year',
            width:'200px'
        },
        {
            type: 'text',
            title:'Price',
            width:'100px',
            mask:'#.##',
        },
        {
            type: 'checkbox',
            title:'Automatic',
            width:'100px'
        },
    ],
    updateTable: function(el, cell, x, y, source, value, id) {
        if (x == 2 && y == 2) {
            cell.classList.add('readonly');
        }
    } 
});

(4). Lazy loading

データ量が多いCSVやJSONデータなどは、lazyLoading:true, loadingSpin:trueとしておけば100行スクロールする毎に次のページをレンダリングしている感じになっている。

<div id="spreadsheet"></div>

<script>
jexcel(document.getElementById('spreadsheet'), {
    csv:'https://bossanova.uk/jexcel/demo1.csv',
    csvHeaders:false,
    tableOverflow:true,
    lazyLoading:true,
    loadingSpin:true,
    columns: [
        {
            type:'text',
            width:200,
            title:'Name'
        },
        {
            type:'dropdown',
            width:100,
            title:'Age',
            source:[
            {
                id:1,name:'Male'
            },
            {
                id:2,
                name:'Female'
            }]
        },
        {
            type:'text',
            width:200,
            title:'City'
        },
     ]
});
</script>

(5). Advanced dropdown

ページの「2 CONDITIONAL DROPDOWN」
条件付きDrop Downの機能もある。実装も分かりやすくてよい。

var data2 = [
    [3, 'Cheese', true],
    [1, 'Apples', true],
    [2, 'Carrots', true],
    [1, 'Oranges', false],
];

dropdownFilter = function(instance, cell, c, r, source) {
    var value = instance.jexcel.getValueFromCoords(c - 1, r);
    if (value == 1) {
        return ['Apples','Bananas','Oranges'];
    } else if (value == 2) {
        return ['Carrots'];
    } else {
        return source;
    }
}

jexcel(document.getElementById('spreadsheet2'), {
    data:data2,
    columns: [
        { type:'dropdown', title:'Category', width:'300', source:[ {'id':'1', 'name':'Fruits'}, {'id':'2', 'name':'Legumes'}, {'id':'3', 'name':'General Food'} ] },
        { type:'dropdown', title:'Food', width:'200', source:['Apples','Bananas','Carrots','Oranges','Cheese'], filter:dropdownFilter },
        { type: 'checkbox', title:'Buy', width:'100' },
    ],
    onchange:function(instance, cell, c, r, value) {
        if (c == 0) {
            var columnName = jexcel.getColumnNameFromId([c + 1, r]);
            instance.jexcel.setValue(columnName, '');
        }
    }
});

(6). Clock Picker

Clock Pickerはjquery-clockpickerを利用します。

<html>
<script src="https://bossanova.uk/jexcel/v3/jexcel.js"></script>
<script src="https://bossanova.uk/jsuites/v2/jsuites.js"></script>
<link rel="stylesheet" href="https://bossanova.uk/jexcel/v3/jexcel.css" type="text/css" />
<link rel="stylesheet" href="https://bossanova.uk/jsuites/v2/jsuites.css" type="text/css" />

<link rel="stylesheet" type="text/css" href="http://weareoutman.github.io/clockpicker/dist/jquery-clockpicker.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="http://weareoutman.github.io/clockpicker/dist/jquery-clockpicker.min.js"></script>

<div id="custom"></div>

<script>
var data2 = [
    ['PHP', '14:00'],
    ['Javascript', '16:30'],
];

var customColumn = {
    // Methods
    closeEditor : function(cell, save) {
        var value = cell.children[0].value;
        cell.innerHTML = value;
        return value;
    },
    openEditor : function(cell) {
        // Create input
        var element = document.createElement('input');
        element.value = cell.innerHTML;
        // Update cell
        cell.classList.add('editor');
        cell.innerHTML = '';
        cell.appendChild(element);
        $(element).clockpicker({
            afterHide:function() {
                setTimeout(function() {
                    // To avoid double call
                    if (cell.children[0]) {
                        myTable.closeEditor(cell, true);
                    }
                });
            }
        });
        // Focus on the element
        element.focus();
    },
    getValue : function(cell) {
        return cell.innerHTML;
    },
    setValue : function(cell, value) {
        cell.innerHTML = value;
    }
}

myTable = jexcel(document.getElementById('custom'), {
    data:data2,
    columns: [
        { type: 'text', title:'Course Title', width:300 },
        { type: 'text', title:'Time', width:100, editor:customColumn },
     ]
});
</script>
</html>

4. まとめ

一覧での編集機能が一通りそろっていて、実装もかなりわかりやすく、気に入りました。大量データの場合はSlickGridのほうが軽いと感じましたが、SlickGridはちょっと実装が難しいので、ニーズに合わせて使っていきたいと思いました。

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

10秒ごとにアラートする、ボタンで開始、停止するには

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>5</title>
  </head>
  <body>
    <p>10秒毎にアラート</p>
    <button onclick="startTimer()">Start</button>
    <button onclick="stopTimer()">Stop</button>
  </body>
</html>
<script type="text/javascript">
      function timeout_callback() {
        alert("timeout!");
      }

      var timer = null;
      function startTimer() {
        if (timer == null)
          timer = setInterval(timeout_callback, 10000);
      }
      function stopTimer() {
        if (timer != null)
          clearInterval(timer);
          timer = null;
      }
    </script>
  • if (timer != null) clearInterval(timer); timer = null; でタイマーを停止
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

入力した文字を1秒ごとに、1文字ずつ増やしてpタグに表示させるには

<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <p id="pppp"></p>
    <input type="text" id="ppp" value="入力してください">
    <button onclick="startTimer()">Start</button>
  </body>
</html>
 <script type="text/javascript">
      //var str = new String(s);
      var n;
      function word(){
        var s = document.getElementById("ppp").value;
        var len = s.length;
        document.getElementById('pppp').innerHTML=s.slice(0,n);
        if(n<len){
          n++;
        } else{
          clearInterval(intervalId);
          s=null;
          }
      }
      function startTimer(){
        n=1;
        intervalId=setInterval(word,1000);
      }
    </script>
  • var n;で関数の制限
  • s.slice(0,n);で0番目とn番目の文字を取り出す
  • setInterval(動作,間隔);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RemoteJSでリモートブラウザのデバッグをする

はじめに

Webサービスの開発をしていると、要件によっては様々なデバイス、ブラウザをサポートしなければならないことがあります。動作確認時や不具合の検証をする際は、それらのブラウザの開発ツールなどを使用してコンソール上のメッセージやDOMを確認することと思いますが、そのような手法が使えないケースも存在します。

モバイル端末における動作を確認したいけどケーブルが無い、検証対象のブラウザが機器に組み込まれたものである、などのパターンがそれに該当することでしょう。そして、そのような場合の動作確認は決して容易ではありません。SentryやRollbarといったバグトラッキングを使用して細かく情報を送信するという方法も考えられますが、インタラクティブな情報取得はできないため、何度もトライアンドエラーを繰り返すことになってしまう可能性もあります。

そこで解決策の1つになり得るのが RemoteJS です。

RemoteJSとは

RemoteJSは、動作検証をしたいブラウザとの間でセッションを確立することで、対象のブラウザでJSコードの実行といった操作を行うことが可能になります。

image.png

RemoteJSを使用することで、以下のようなことが実現できます。

  • ブラウザでの任意のJavaScriptコードの実行
  • スナップショットの取得
  • 発生したイベントの監視

また、これらの機能は無料で利用することが可能です。

それでは、以下のセクションで使い方を見てみましょう。

使い方

セッションの確立

RemoteJSを利用するには、 トップページ の「Start Debugging」のボタンをクリックします。これによって、エージェントをインストールするためのコードが含まれたモーダルが表示されるので、必要なものをコピー&ペーストして使用します。

このモーダルでは、 TagJavaScriptBookmarklet のいずれかを選択することが可能であり、それぞれ次のように利用します。

種類 用途
Tag Webページ(HTMLファイル)に埋め込むことが可能な script タグです。このscriptにより、必要なJavaScriptコードのダウンロードと実行が行われます。以下に示すいずれの方法も使えない場合などに有用です。
JavaScript 検証対象のブラウザのコンソールで実行するJavaScriptコードです。コードをコンソールから実行するだけなので、非常に簡単に検証を開始することができます。
Bookmarklet ブラウザのブックマークレートとして使用可能なコードです。

image.png
モーダルの表示例

RemoteJSによるデバッグ

上述したいずれかの方法で検証対象のブラウザとのセッションを開始すると、RemoteJSのデバッグ機能が利用可能となります。ここではいくつかの機能を見てみましょう。

JavaScriptコードの実行

RemoteJSの画面下部のエリアでは、任意のJavaScriptコードを実行することができます。例えば、以下のような button 要素の状態を確認したいとしましょう。

image.png

そのために、以下の querySelector を実行して要素を取得してみます。

document.querySelector('button.start-button')

すると、次のように対象のブラウザにおけるコードの実行結果が表示されます。

image.png

さらに、次のようなコードをRemoteJSから実行すると、

image.png

ブラウザ側の表示結果も変更されました。

image.png

このように、RemoteJSを利用することで、ローカルブラウザのコンソールを使用した場合とほぼ同等のインタラクションが可能となります。これによって、特定のオブジェクトの値の確認や、任意のイベントをトリガーさせることもでき、問題の特定に役立ちます。

スナップショット

RemoteJS上部のエリアからはスナップショットを撮影することも可能であり、リモートブラウザでの表示内容を確認することもできます。

最後に

この記事では、RemoteJSを利用することでデバッグが困難な端末、ブラウザでリモートデバッグする方法を紹介しました。通常の開発ツールや、バグトラッキングシステムを利用したトラブルシュートが最も一般的な不具合の原因究明方法となると思いますが、それらの手段が利用できない時の手段として、RemoteJSを選択肢に入れておくと良いかもしれません。

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

JavaScriptでチャットボットを作る

JavaScriptでチャットボットを作る

はじめに

今回作るのは、簡単に質問応答してくれるチャットボットです。
言語はHTML,CSS,JavaScript(約50行)です。
応用次第で、より複雑なものを作ることも全然可能です!

目標

  • 自分の名前を送信したら、それを含めたメッセージが返す
  • 自分にランダムなメッセージを返す
  • (様々な会話パターンを作ってみる)
  • (相手から選択肢のボタンを送信され、それぞれに応じた結果を返す)

この記事の対象者

JavaScriptで様々なものを作ってみたい人。チャットボットやAIに興味がある人などなど。

ここでは必要最低限の部分だけ説明します。詳細は下のCodePenをご覧ください

See the Pen チャットボット by MF3PGM (@masa_mf3rs) on CodePen.

HTML

チャットの画面にあるulに(JavaScriptで)liを追加していく仕組みです。

HTML
<!--画面-->
<div id= "field">
    <ul id= "chat-ul"></ul>
</div>

<!--入力場所,送信ボタン-->
<div id= "input-field">
    <input type= "text" id= "chat-input">
    <input type= "button" value= "Send" id= "chat-button" onclick= "btnFunc()">
</div>

CSS

☞ポイント

  • ulの子要素li(背景色:緑または白の部分)に直接文字を書いてもいいですが、デザイン上、今回はliの子要素divに文字を書いていきます。(吹き出しテキストのpaddingやline-height,改行などが複雑になるのを防ぐため)

  • 話す人が自分の場合、liに chat-rightクラスを付与し、話す人が相手ならchat-leftクラスを付与します。

CSS
/*チャット画面のスクロール設定*/
#field{
    /*横向きのスクロール禁止*/
    overflow-x: hidden;
    /*縦向きのスクロール許可*/
    overflow-y: scroll;
    /*スマホでは指でスクロール*/
    -webkit-overflow-scrolling: touch;
}

/*自分の吹き出し*/
.chat-right{
    margin-left: auto;
    margin-right: 25px;
    background: #A4E496;
}
/*相手の吹き出し*/
.chat-left{
    margin-left: 25px;
    background: #FFF;
}

JavaScript

今回は、相手のメッセージは全て2秒遅れにします。

☞ポイント

  • 相手が返信する度に、chatCountを1ずつ足していきます。自分がテキストを入力し、送信ボタンを押した瞬間のchatCountの値に応じて、配列から特定のメッセージを選び返信させます。
  • 今回作った関数は、画面に吹き出しを描画するoutput()と、送信ボタンを押したときのbtnFunc()です。
JavaScript
//相手の返答内容
const chat = [
    'Hello ! Welcome to AI chat !',
    'What is your name ?',
    'How are you today ?',
    [['Alright !'], ['Oh really!'], ['Ok!']]//ランダムな返答
];

//相手の返信の合計回数(最初は0)
//これを利用して、自分が送信ボタンを押したときの相手の返答を配列から指定する
let chatCount = 0;

//画面への出力をする関数(valはメッセージ内容,personは誰が話しているか)
function output(val, person) {
    //中略
    if(person === 'other'){
        //中略
        chatCount++;
    }
}

//自分がテキストを入力し、ボタンを押したときの関数
function btnFunc() {
    //中略
    output(inputText.value, 'me');
    //中略

    //相手の送信の合計回数に応じて次の返信を指定
    switch(chatCount) {
        //もし相手のトーク数が2個の時に送信ボタンが押されたら、
        //自分の名前を含んだメッセージと、chat配列の2(3個目)が返信
        case 2:
            output('Hi, ' + inputText.value + ' !', 'other');
            setTimeout( ()=> {
                output(chat[2], 'other');
            }, 2000);
            break;

        //もし相手のトーク数が4個の時に送信ボタンが押されたら、
        //chat配列の3(4個目)のランダム番目が返信
        case 4:
            output(chat[3][Math.floor(Math.random() * chat[3].length)], 'other');
            break;
    }
}

//最初に相手から話しかけられる
output(chat[0], 'other');
setTimeout( ()=> {
    output(chat[1], 'other');
}, 2000);

おわりに

チャットボットを作りたい。。しかし、どのサイトを調べても、ライブラリやサーバ,ChatUIなどを使った難しいものばかりでした。そんな中、ピュアなJavaScriptだけで作れないかと数日間試行錯誤し、やっと完成しました。
まだ初心者なので、アドバイス等あれば教えていただければと思います。
そして、この記事が皆様にお役に立てれば幸いです。

参考書

本当によくわかるJavaScriptの教科書P.122(outputメソッドの2つの引数を使った部分)

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

Vue.jsでローカルからcsvを持ってくるやつ(not File API)

まえおき

一か月でVue使って何か作る企画のはずでしたが、なんか忙しそうなので記事の方は色々やってお茶を濁します。
アプリはちゃんと作ります。

経緯

Vue.jsでローカル(サーバー)においてあるcsvを持ってこようとしたら
みんなFileAPIのことばっか説明しててイラついたので書きました。

そういうの(いちいちGUIでcsvファイルを選ぶ)がやりたいんじゃないんだよ!!!

やる事

スクリーンショット 2019-11-25 10.58.05.png

こういうのをローカルサーバーのディレクトリ直下において読み込みます

結論

axios使っとけ

やり方

こうして

yarn add axios

こう

import Vue from "vue";
import axios from "axios";

new Vue({
    el: '#app',
    data () {
      return {
        csv: null
      }
    },
    mounted () {
        let source = "./test.csv" //読みたいcsvのパス

        axios
        .get(source)
        .then(res =>{
            // csvファイルの内容を取得
            let text = res.data;  

            //csvファイルをバラす(やりたい事によって違うので参考程度に)
            let csvData = []
            let splited = text.split("\n");
            splited.forEach(row =>{
                csvData.push(row.split(","))
            });

            //作ったデータを入れる
            this.csv = csvData;
        });
    }
})

別にVueじゃないねこれ

ところで

Q. なんでXMLHttpRequest使わなかったの?
A. そう言うのもあるんだ

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

Vue.jsでローカルからcsvを持ってくるやつ(not File API)

まえおき

一か月でVue使って何か作る企画のはずでしたが、なんか忙しそうなので記事の方は色々やってお茶を濁します。
アプリはちゃんと作ります。

経緯

Vue.jsでローカル(サーバー)においてあるcsvを持ってこようとしたら
みんなFileAPIのことばっか説明しててイラついたので書きました。

そういうの(いちいちGUIでcsvファイルを選ぶ)がやりたいんじゃないんだよ!!!

やる事

スクリーンショット 2019-11-25 10.58.05.png

こういうのをローカルサーバーのディレクトリ直下において読み込みます

結論

axios使っとけ

やり方

こうして

yarn add axios

こう

import Vue from "vue";
import axios from "axios";

new Vue({
    el: '#app',
    data () {
      return {
        csv: null
      }
    },
    mounted () {
        let source = "./test.csv" //読みたいcsvのパス

        axios
        .get(source)
        .then(res =>{
            // csvファイルの内容を取得
            let text = res.data;  

            //csvファイルをバラす(やりたい事によって違うので参考程度に)
            let csvData = []
            let splited = text.split("\n");
            splited.forEach(row =>{
                csvData.push(row.split(","))
            });

            //作ったデータを入れる
            this.csv = csvData;
        });
    }
})

別にVueじゃないねこれ

ところで

Q. なんでXMLHttpRequest使わなかったの?
A. そう言うのもあるんだ

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

javascript 基本その3-2

前回の内容から続いて
書き切れなかった内容をかきます。

タブの切り替え

結構重要な機能である
タブの切替機能について書きます。

まず例として下記の様なコードを書きます。
次にjsの詳細なコード内容についても書いて行きます。

html
 <!--イベントを発火させたいクラスをまずそれぞれ定義します。-->
<div class="panel">
    <!--タブ-->
    <ul class="tab-group">
        <li class="tab tab-A is-active">タブ-1</li> <!-- is-activeがポイント -->
        <li class="tab tab-B">タブ-2</li>
        <li class="tab tab-C">タブ-3</li>
    </ul>
  <!--基本is-activeの時だけcssを当てる様にするのが目的です。-->

    <!--タブを切り替えて表示するコンテンツ-->
  <div class="panel-group">
        <div class="panel tab-A is-show">コンテンツ-1</div> <!-- is-showがポイント -->
        <div class="panel tab-B">コンテンツ-2</div>
        <div class="panel tab-C">コンテンツ-3</div>
     <!--is-showだけ表示、他は非表示という形にします。-->
    </div>
</div>
css
.tab-group{
    display: flex;
    justify-content: center;
}
.tab{
    flex-grow: 1;
    padding:5px;
    list-style:none;
    border:solid 1px #CCC;
    text-align:center;
    cursor:pointer;
}
.panel-group{
    height:100px;
    border:solid 1px #CCC;
    border-top:none;
    background:#eee;
}
.panel{ /*非表示にさせます*/
    display:none;
}
.tab.is-active{  /*tabを上書きして発火させます*/
    background:#F00;
    color:#FFF;
    transition: all 0.2s ease-out;
}
.panel.is-show{ /*panelを上書きしてこれだけ表示させます*/
    display:block;
}
js
document.addEventListener('DOMContentLoaded', function(){
    // タブに対してクリックイベントを適用
    const tabs = document.getElementsByClassName('tab'); //ノードを取得
    for(let i = 0; i < tabs.length; i++) { //繰返し構文で全てに適用
        tabs[i].addEventListener('click', tabSwitch);
    }

    // タブをクリックすると実行する関数
    function tabSwitch(){
        // タブのclassの値を変更
        document.getElementsByClassName('is-active')[0].classList.remove('is-active'); //is-activeの追加
        this.classList.add('is-active'); //is-activeの削除
        // コンテンツのclassの値を変更
        document.getElementsByClassName('is-show')[0].classList.remove('is-show');
        const arrayTabs = Array.prototype.slice.call(tabs);
        const index = arrayTabs.indexOf(this);
        document.getElementsByClassName('panel')[index].classList.add('is-show');
    };
});

DOMContentLoadedイベント

最初の HTML 文書の読み込みと解析が完了したとき、スタイルシート、画像、サブフレームの読み込みが完了するのを待たずに発生します。

this

関数(tabSwitch)の中でthisを使うと、イベントの発生元となった要素を取得することができます。

Array.prototype.slice.call()

引数にとったオブジェクトを配列に変換してくれます。

indexOf()

inexOf()は配列に対してだけ使い、DOMを引数にとって一致した要素番号を戻します。

forEach()

配列に対してよく使われる繰り返し処理です。

foreach1
配列.forEach( コールバック関数 )
foreach2
配列.forEach( function(value, index) {
  // 配列のデータに対しての処理、valueは値、indexは要素番号
} )
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LoLチャンピオン言えるかな?を作った結果

シンデレラガールズ言えるかな?に触発されリーグオブレジェンドのチャンピオン(9.23)言えるかな?を作成してみた

LoLチャンピオン言えるかな?
https://masajiro999.github.io/ierukana/

jsonデータが全然違うので少し戸惑ったけど配列か連想配列かの違いくらい

Macで作成、環境を紹介

Githubデスクトップ(gitコマンドなんて知らねーよ奴向け)
https://desktop.github.com

Atom(github製で揃えたい)
https://atom.io

そうそうチャンピオンのjsonはダウンロードしてくるんだぞ
https://riot-api-libraries.readthedocs.io/en/latest/ddragon.html

バージョンアップの都度ここからゲットしてくるぞい

以上

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

【React】はじめてReact触ってみた!~create-react-app編~

概要

JavaScriptを勉強しているときにReactなるものの存在を耳にした。同期にもReactを使って活躍している人もいるので、この機会に少しお勉強したいということでその備忘録。
Twitterで「Reactおちえて」って呟いたら、同期でProgateよりもSEOの高いReactの記事書いている子がいたので、その子の記事を参考にしました。この場を借りてお礼申し上げます。
その子のサイトはこちら→どらごんテック

Reactとは

Facebookが作ったJavaScriptのビュー・ライブラリのこと。
SPA(Single Page Application)への変化とともに需要が高まっている。
ReactがjQueryという麻薬から我々を解放してくれる、そんなお話。(抜粋)
堅苦しい定義は、優秀な先人の方々に任せたいなって...

これからReactを勉強する人が最初に見るべきスライド7選
出来る限り短く説明するReact.js入門
今から始めるReact入門 〜 React の基本

開発環境

Windows 10
npm(ver6.9.0)

とりあえず動かしてみる

優秀な同期の一人の@atsuo1023に、Reactはとりあえず
create-react-app <プロジェクト名>
しとけば、難しい環境構築なしにできると聞いたのでやってみました。
まずは、create-react-appのコマンドが打てるように、npm(Node Package Manager)を使って
npm install -g create-react-app
をして早速create-react-app testでGO!!!
create-react-app-error.jpg
・・・嘘やん...

create-react-app test打つだけでいいって言うたやん。こんなんあんまりや...なんでコマンド動かないん?
...私気になりますっ!

色々調べて、試してみました。
・同じディレクトリ下に以前友達がcreate-react-appで作ったフォルダがあったので、ディレクトリを変更 → 意味なし
npm -vでローカルにもnpmがあるか確認 → 6.9.0 あった
create-react-app --versionで確認 → 3.2.0 あった
npm update -g npmでバージョン確認 → 反応なし(最新?)

結果...
npm cache clean --forceで直った!!
キャッシュをクリアするってことらしい!!よかった~!(参考記事)
create-react-app-success.jpg

ベースとなるプロジェクトが作成できたので、ディレクトリを移動(cd <プロジェクト名>)してnpm startをたたきます!
すると、
execution_cm.jpg
となり、自動でブラウザが開きます。
execution-result.jpg

時間はかかりましたがなんとかReactなるものを動かすことができました!
次回以降、Clickの動作を加えたり、いろいろ試してアウトプットしていきます!!

補足(npmをインストールしていない方へ)

しっかり版
簡易版
npmはNode.jsに付随するものっぽいので、必要なのはNode.jsのインストールかと!
入ってるかわからないときは、とりあえずコマンドプロンプトで
node --version or npm --versionをたたいてみましょう!

参考リンク

新しい React アプリを作る
Create React Appで環境構築 – React入門
create-react-appを使ってReactコンポーネントの素振り、GitHub Pagesへのデプロイまで
Github issue
知っていると捗るcreate-react-appの設定

おわりに

環境構築ってとても難しい気がしていて、そこで挫折しかけることなんてざらにあると思います。
今回扱ったcreate-react-appは環境構築がほとんどいらないので、とりあえず触ってみたい初学者にとっては最高のものだと思います!
是非皆さんも試してみてください!
また、この記事で間違っているところがありましたら、教えていただけると大変助かります!
最後まで見ていただきありがとうございました。

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

短絡評価を用いたReactでのコンポーネント配置

はじめに

いなたつアドカレへようこそ初日です、頑張って一人で25記事書くODL(あうとぷっとどりぶんらーにんぐ)企画、完走したいですね。

自分の中にあるネタを引っ張り出して記事を書いていきます。。。くおりてぃ。。。。

今日は頭にあるとスマート?に実装をかける?やつを忘れないようにここにのこしておきます。

短絡評価とは

andやorで値を評価する時に

A && B // A,B共に真
A || B // A,Bどちらかが真

となるためA && Bの A が偽の場合やA || BのAが真の場合、つまりBの値にかかわらずA && BA || B の評価結果が決定する場合にはBを評価しないといった言語使用です。

主な使用ケースとしては、Bに動作の重い処理を置いて、Aの値で評価が決定してしまい、Bの結果が不要な(重い処理を走らせる必要がない)場合に処理を軽減する

今回はこの性質を利用し、ある条件が真となる場合にのみコンポーネントを配置するといったことを行う。

じっそー

結論から言ってこんな感じ

{i % 2 == 0 && <div>test</div>}

上のプログラムはiが偶数の時に<div>test</div>を配置するといった内容です。

A && Bの A が偽の場合やA || BのAが真の場合、つまりBの値にかかわらずA && BA || B の評価結果が決定する場合にはBを評価しないといった言語使用です。

&&の場合の性質の応用ですね、Aが偽の場合はたとえBが真であっても、結果は偽となるため、Bを評価しないと言った性質なので、iが奇数の場合は右の式が評価されず、<div>test</div>は配置されません。
しかし、iが偶数の場合はAが真なので、Bを評価しないと、式全体の評価ができないため、Bが評価され、<div>test</div>が配置されます。

おわりに

このようにして、とある条件が真の場合のみ、コンポーネントを配置することができます。
実際の実装では、三項演算子を用いて、条件が真の場合と偽の場合で配置するコンポーネントを分けると言った場面が多いかもしれませんが(知らんけど)&&の短絡評価の性質を利用することで、条件が真の場合のみコンポーネントを配置すると言った実装がスマート?にかけます。

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

年末まで毎日webサイトを作り続ける大学生 〜44日目 イベント(oncontextmenu, onresize, onerror, onmouseover/onmouseout)を使ってみる〜

はじめに

こんにちは!@70days_jsです。
もう12月ですね。

今日はJavaScriptのイベントを使ってみました。

  1. oncontextmenu
  2. onresize
  3. onerror
  4. onmouseover/onmouseout

今日は44日目。(2019/12/1)
よろしくお願いします。

サイトURL

やったこと

今日使ったイベントは主に4つです。

  1. oncontextmenu(赤・青色)
  2. onresize(黄色)
  3. onerror(緑色)
  4. onmouseover/onmouseout(すべて)

test2.gif

1. oncontextmenuの部分

oncontextmenuは右クリック時のイベントです。

赤色のボックスでは右クリックを禁止して(falseにする)、赤色の枠の中に文字列を表示するようにしています。

青色のボックスでは右クリックされるとalertが表示されます。

<div id="contextMenu" class="context-menu onmouseover">右クリックをすると・・・?</div>
<div id="contextMenu2" class="context-menu2 onmouseover">右クリックをすると・・・?</div>
let contextMenu = document.getElementById('contextMenu');
let contextMenu2 = document.getElementById('contextMenu2');
contextMenu.oncontextmenu = function () {
    contextMenu.innerHTML = '右クリック不可エリア(不可ではない)';
    return false;
}
contextMenu2.oncontextmenu = function () {
    contextMenu2.innerHTML = ('alertの表示!');
    alert('右クリックをしたな?');
}

これで右クリックを使えなくすることもできますが、JavaScript自体を禁止されたり、ディベロッパーツールでコードを書き換えられたりしたら右クリックができるようになります。

2. onresize

onresizeは画面サイズの変更時のイベントです。

今回は画面サイズが変更されたら横幅の値を表示するようにしています。

<div id="resize" class="resize onmouseover">Displayサイズは・・・? <br></div>
let resize = document.getElementById('resize');
window.onresize = function () {
    resize.innerHTML += window.innerWidth + ',';
}

window.innerWidthで横幅を取っています。

3. onerror

onerrorは画像読み込み失敗時のイベントです。

gifでは猫の画像を表示していますが、htmlのソースコードには存在しないURLを書いています。

<div id="onerror" class="onerror onmouseover">
    画像が表示されない場合は・・・?<br>
    <img src="day44.png" id="onerrorImage">
</div>
let onerror = document.getElementById('onerror');
let onerrorImage = document.getElementById('onerrorImage');
onerrorImage.onerror = function () {
    onerrorImage.removeAttribute("src");
    onerrorImage.setAttribute('src', 'day31_image/5.jpg');
}

setAttributeで画像が存在するURLを書いています。

4. onmouseover/onmouseout

onmouseoverはマウスが乗った時のイベントです。
onmouseoutはマウスが離れた時のイベントです。

今回は、ボックスにマウスが乗ったらborder-widthを太くして目立つようにしました。
要素の取得はクラスで行いました。

let onmouseover = document.getElementsByClassName('onmouseover');
for (var i = 0; onmouseover.length > i; i++) {
    onmouseover[i].onmouseover = function (e) {
        e.target.style.borderWidth = '.5em';
    }
    onmouseover[i].onmouseout = function (e) {
        e.target.style.borderWidth = '1px';
        console.log('test');
    }
}

ボックスにクラスをつけておき、getElementsByClassNameでクラスを取得します。
for文で回して、e.targetでそれぞれのborder-widthの大きさを変更しています。

メモ

  • 2つのオブジェクト
    • ビルトインオブジェクト・・・jsに組み込まれた機能をオブジェクト化(ECMAScript仕様に基づく)、配列、文字列、正規表現など
    • ナビゲーターオブジェクト・・・ブラウザ自体の情報、表示に関わる機能をオブジェクト化(ブラウザ独自の実装がされている)

感想

イベントを調べていると、どうもweb業界の闇が見えるなぁと思っていたんですが、イベントはナビゲーターオブジェクトだからブラウザごとにメソッド名・プロパティ名が違って当たり前なんですね。
標準化して欲しいところではありますが、こればかりは時間がかかりそうな気がする・・・。イベント便利なんだけどな。

最後までお読みいただきありがとうございます。明日も投稿しますのでよろしくお願い致します。

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

一部のWebサイト高速表示に一役買っている「InstantClick」について

前書き

こちらはAteamFinergyのアドベントカレンダー3日目の記事になります。
内容としてはInstantClickの説明が大半を締めており
目的としては、InstantClickの認知を増やす事です

InstantClickとは

Webサイトを劇的に高速化するJavaScriptライブラリ」
-- from instantclick.io

JavaScriptのライブラリにInstantClickというものがあり、Webサイトに対して適用すると高速になる...というものがあり
そのInstantClickを実際に使用している例を上げると「dev.to」や「FC2爆速テンプレート」などで使用されています

特にdev.toについては阿部寛のホームページと比較している記事が多く見られ、ご存知のかたは多いのではないでしょうか

InstantClickではpjax(pushState + ajax)を使用して高速化を図っており
リンク先のHTMLをpreloadしておき、リンク先へ遷移するタイミングに「head, body, title」を入れ替え、ユーザーに対してとても素早いページ遷移の体験を提供している様です。

pjaxは、ajaxを介してサーバーからHTMLを取得し、ページ上のコンテナ要素のコンテンツを読み込まれたHTMLに置き換える
-- from jquery-pjax

ブラウザ上のページを変更することはないがInstantClickによってHTML要素が変更され、結果SPAに近しい挙動となります

仕組みについて

リンク先のHTMLをpreloadすると言っても、Webサイトには無数のリンクがあるため、「どのタイミング」で「どのくらい」取得する事が大事になるはずです

InstantClickのデフォルトの設定では、遷移先のリンクに対して「マウスを重ねた瞬間」に「リンク先の要素」を取得するようです

実際にユーザーがリンクにマウスオーバーしてからクリックするまでの間に200~300ms経過するため、その間にリンク先をpreloadしておく事でスムーズな遷移を提供しています

実際にこちらで、「MouseOverイベントが走ってからクリックするまでの秒数」と「MouseDownイベントが走ってからクリックするまでの秒数」を確認する事ができるので一度見てみると良さそうです

また、モバイルについてはmouseoverではなく、touchstartイベントが対象となっているようです

実際にこちらを見ていただくと検証画面のInitiatorの箇所がInstantClickによって遷移先のHTMLを取得している事が確認できます
test.gif
FC2爆速テンプレートdemoページより

導入方法

導入の仕方についてはとても簡単で
「InstantClickをダウンロード」「ファイルを任意の箇所に配置」
「読み込みのscriptタグと初期化のscriptタグを記述」の3ステップで出来ます。

  1. こちらからProduction Versionをダウンロード
  2. InstantClickの読み込みをページの下部で実施
  <script src="instantclick.min.js" data-no-instant></script>
  <script data-no-instant>InstantClick.init();</script>
</body>

注意点

InstantClickを使用する際にはいくつか注意する事があります

JSで取得する事ができるイベントについて

InstantClickはpjaxの技術でHTMLを入れて表示を切り替えるので
DomContentLoaded jQueryのready() などのイベントが発火しなくなります。

そのため、InstantClickではいくつかの既存のライフサイクルをフックする事で解決します。

解決法: 既存ライフサイクルのフック

InstantClickでは下記の4つが提供されており、それぞれ元のイベントの代替になります。

  • change
    • DomContentLoadedの置き換えとなり、InstantClickが読み込まれていない最初の遷移時にも発火
  • fetch
    • マウスオーバーした時に、遷移先のHTMLをpreloadするタイミングで発火
  • recieve
    • preloadが完了したタイミングで発火
  • await
    • ユーザーがクリックしたがpreloadが完了していない待ち時間の間に発火

導入した際には
上記の様なイベントに既存のイベントが置き換わるため注意が必要です

サーバーへのリクエスト数

デフォルトの設定のままだと、マウスオーバーするだけでサーバーへリクエストを送る事になり、場合によってはサーバーへの負荷が高まることやブラウザ側では使用しないムダなHTMLを取得する事になります

そのため、InstantClickでは2つの解決策を用意しています。

  1. マウスダウンイベントへの変更
  2. マウスオーバー時のリクエスト遅延

解決法①: マウスオーバーからマウスダウンへの変更

InstantClickを初期化するタイミングでmouseoverイベントからmousedownイベントに変更する事ができます。

Webサイトによっては、マウスオーバー程効果を得る事が出来ない可能性はありますが
それでもマウスダウンからクリックするまでに80~120ms程度かかるのでその間に普段のページ遷移より体感としては早く行えます。
計測はこちら

初期化の記述にmousedownを引数として渡してあげるだけで変更できるようです

 <script data-no-instant>InstantClick.init('mousedown');</script>

実際に下記のコードを確認すると
初期化のタイミングでこの引数を渡すとdocumentに対してmouseovermousedownかのどちらのイベントをリッスンするか変更する事が確認できます
https://github.com/dieulot/instantclick/blob/master/src/instantclick.js#L804
https://github.com/dieulot/instantclick/blob/master/src/instantclick.js#L830

解決法②: マウスオーバー時のpreloadリクエストの遅延

デフォルトとマウスダウンへ変更した時とのちょうど間にある選択肢として
「マウスオーバー時にリクエストは送るけれど、カーソルが触れただけの時とかは無しにしたい」というケースでは
マウスオーバー時のリクエストを遅延させる事で解決できます

こちらを適用するには、マウスダウンへ変更した時と同様に、初期化のタイミングで何ms遅延させたいかを渡してあげるだけで大丈夫です
下記の例は75ms遅延させる時

 <script data-no-instant>InstantClick.init(75);</script>

実際のコードを確認すると
初期化で渡された引数が保存され、setTimeoutによって設定した時間分遅れてpreloadを実行するようです。
https://github.com/dieulot/instantclick/blob/master/src/instantclick.js#L454

ただ何も渡さない場合は65msにpreloadするようです。
https://github.com/dieulot/instantclick/blob/master/src/instantclick.js#L30

リンク毎のpreload制御

初期設定だと一部を除いてほとんどのリンクがpreloadの対象となります。

具体的には下記の様なリンクがpreloadの対象外となります
- targetまたはdownload属性を所持している
- 現在のWebサイトとは別のdomainまたはprotocol
- 同じページへのanchor

<a href='https://qiita.com/' target='_blank'>not preload</a>
<a href='https://qiita.com/' download>not preload</a>
<a href='https://github.com/'>not preload</a>
<a href='http://qiita.com/'>not preload</a>
<a href='#InstantClickとは'>not preload</a>

ただ、使用する際に「ほとんどのリンクは適用したいけれど、このリンクはpreloadしたくない」などのケースが出てくる時があるでしょう
その時は「このリンク適用しない」と設定が可能な、ブラックリスト機能があります

解決法: ブラックリスト・ホワイトリスト機能

設定方法は簡単で、preloadして欲しくないリンクに対してdata-no-instant属性を付与するだけです。

<a href='https://qiita.com/' data-no-instant>not preload</a>

また、該当箇所が複数あった場合については親の要素にdata-no-instant属性を付与する事ですべての子要素に対してpreloadが走らなくなります。

<div data-no-instant>
  <a href='https://qiita.com/'>not preload</a>
  <a href='https://qiita.com/'>not preload</a>
  <a href='https://qiita.com/'>not preload</a>
</div>

複雑化した際の方法としては
1. 親要素にdata-no-instantを記述して子要素をまとめてブラックリスト追加
2. その子要素の任意のものだけdata-instant属性を追加する
とする事でホワイトリストに追加し簡易的な対応ができます

<div data-no-instant>
  <a href='https://qiita.com/'>not preload</a>
  <a href='https://qiita.com/' data-instant>preload</a>
</div>

まとめ

  • InstantClickはpjaxで遷移先の値をpreloadし、入れ替える
  • 遷移先を取得するのはmouseoverイベントのタイミングなのでユーザーに素晴らしい体験を提供できる
  • サーバーへの負荷が高まる可能性がある
  • javascriptの元々のイベントを取得できなくなる

いかがだったでしょうか、InstantClick自体が少し古い技術かと思いますが「dev.toめっちゃはやいやんけ!」という仕組みの片鱗を知って、興味を持っていただけたら幸いです

引用元

http://instantclick.io/
https://developer.mozilla.org/ja/
https://dev.to/
https://fc2information.blog.fc2.com/
https://github.com/defunkt/jquery-pjax

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

create-react-appで生成したReactのESLintをVSCodeでやる(自動整形付き)

概要

create-react-app用のESLintをVSCodeでやる。

前提知識

ESLint自体はcreate-react-appすると自動的にインストールされている。
で、create-react-appした時のpackage.jsonに↓があるのがポイント。

{
  //...略...
  "eslintConfig": {
    "extends": "react-app"
  },
  //...略...
}

npm run startすると、サーバが起動し、その中のメッセージで自動的にlintして警告を出したりしてくれるが、これはreact-appというルールを使用していることになる。

設定

で、VSCodeにreact-appのlintを同じように使用するためには、

1. VSCodeの拡張にESLintを入れる

そのまま。↓を入れる。
image.png

2. ESLintのファイルを生成する

ctrl + shift + P で、ESLint: create ESLint congiguration を選択する。
ESLintの設定ファイル生成のコマンドラインが起動するので、↓を参考に設定。
image.png

ワークスペースのルートに、eslintrc.jsファイルが生成される。

3. eslintrc.jsのベースルールをreact-appへ変更する

eslintrc.jsのextendsを、

'extends': 'eslint:recommended',

↓へ変更する。

'extends': 'react-app',

4. ファイル保存時に自動的にlintルールで整形されるようにする

ctrl + shift + P で、ユーザー設定を開く を選択する。
ユーザワークスペースへ変更する。
image.png
設定の検索に、ESLintと入力する。
Files: Auto Fix On Save にチェックを入れる。

おまけ

  • セミコロン付与
  • インデントスペース4つ
  • シングルクオート
  • 末尾の空白を削除

のサンプルeslintrc.js

module.exports = {
    'env': {
        'browser': true,
        'es6': true
    },
    'extends': 'react-app',
    'globals': {
        'Atomics': 'readonly',
        'SharedArrayBuffer': 'readonly'
    },
    'parserOptions': {
        'ecmaFeatures': {
            'jsx': true
        },
        'ecmaVersion': 2018,
        'sourceType': 'module'
    },
    'plugins': [
        'react'
    ],
    'rules': {
        'indent': [
            'error',
            4,
            { "SwitchCase": 1 }
        ],
        'quotes': [
            'error',
            'single'
        ],
        'semi': [
            'error',
            'always'
        ],
        'no-trailing-spaces': [
            "error",
            { "skipBlankLines": true }
        ]
    }
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nullとundefinedの違いを忘れるので真剣に向き合ってみた

nullとundefinedの違いって何??

null = 値が空
undefined = 未定義

オブジェクト作成して検証してみた!

const obj = {
  flag: true,
  sample: null // 値を初期化するときに使用する、値が空の意。
}

alert(obj.flag); // true
alert(obj.sample); // null
alert(obj.hoge); // undefined

obj.sample == undefined は 「true」 になる!

nullは、false
undefinedは、false

if(obj.sample == undefined) { // 等価演算子
  alert('true'); // true が出力される
}

obj.sample === undefinedは 「false」 になる!

厳密等価演算子を使用すると、obj.sampleの値は空だが、定義はされているのでfalseとなる。

if(obj.sample === undefined) { // 厳密等価演算子
  alert('true'); // true は出力されない
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Cognitoの画面遷移しないサインインページを作る

久しぶりの、Cognitoネタです。
Cognitoのサインインは、OpenID Connectに準拠しており、サインインは認証エンドポイントにGETを呼び出して画面遷移する必要があります。SPAにおいては画面がリロードされることを意味し、保持していたセッション情報が破棄されることにつながります。

そこで今回は、OpenID Connectでのログインとしつつ、画面リロードを伴わないサインインを実現します。

大まかな流れ

元のページのほかに、ログイン専用のページを作成し、元のページから、ログイン専用ページをロードします。
ログイン専用ページは、別タブや別ウィンドウやポップアップとして開き、ログイン専用ページでログインした結果を元のページに戻します。

(元のページ)→(ログイン専用ページ)(別ウィンドウ)
      認証エンドポイント呼び出し        →(認証プロバイダのサインイン画面)
                          サインイン
      (ログイン専用ページ)(画面リロード)← 認可コードを返却
(元のページ)←認可コードを返却(別ウィンドウをクローズ)
トークンエンドポイントを呼び出して、トークンを取得

後で説明しますが、認可コードを元のページに戻すとき、元のページがログイン専用ページが同じオリジンの場合と、異なるオリジンの場合で多少異なります。

ちなみに、Cognitoのもろもろについては以下を参考にしてください。
 AWS CognitoにGoogleとYahooとLINEアカウントを連携させる

作成したソースコードは以下のGitHubに挙げておきました。
 https://github.com/poruruba/openid_server
 ※\public\proxy のところです。

Cognitoでアプリクライアントを作成

まずは、AWS Cognitoから、アプリクライアントを作成しておきましょう。
今回はresponse_typeとしてAuthorization code grantを使おうと思いますので、クライアントシークレットを生成 はOnにしておきます。

image.png

次に、作成したアプリクライアントで、Authorization code grantを許可しておきます。

image.png

コールバックURLには、これから作成するログイン専用ページのURLを指定しておきます。
また、許可されているOAuthフローには、Authorization code grantを選択しておきます。
許可されているOAuthスコープには、許可するスコープを指定します。とりあえず、email、openid、profileを選択しておきましょうか。

元のページからログイン専用ページをロード

HTML上は特に必要な記載はなく、以下のJavascriptのみを付記しておきます。重要なところだけ抜粋します。

start.js
const REDIRECT_URL = 'ログイン専用ページのUrL';
const CLIENT_ID = 'アプリクライアントID';

var new_win;

        start_login: function(){
            var params = {
                origin : location.origin, // 同じオリジンの場合はコメントアウト
                state: this.state,
                client_id: CLIENT_ID,
                scope: 'openid profile'
            };
            new_win = open(REDIRECT_URL + to_urlparam(params), null, 'width=400,height=750');
        },

REDIRECT_URLが、ログイン専用ページです。
もし、ログイン専用ページと元のページが同じオリジンの場合は、param.originの指定は不要です。
ログイン専用ページのウィンドウサイズは適当に変更してください。ちなみに、ウィンドウサイズはWindowsやMacOSの場合に有効で、AndroidやiOSの場合には、結局は画面いっぱいに表示されます。

ログイン専用ページ

こちらも、Javascriptのみが重要です。

start_login.js
'use strict';

const REDIRECT_URL = 'このページのURL';
const COGNITO_URL = 'https://[ドメイン名].auth.ap-northeast-1.amazoncognito.com';

//var vConsole = new VConsole();

var encoder = new TextEncoder('utf-8');
var decoder = new TextDecoder('utf-8');

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',

    },
    computed: {
    },
    methods: {
    },
    created: function(){
    },
    mounted: function(){
        proc_load();

        if( searchs.code ){
            var state = JSON.parse(hex2str(searchs.state));
            console.log(state);
            var message = {
                code : searchs.code,
                state: state.state
            };
            if( state.origin ){
                            window.opener.postMessage(message, state.origin);
            }else{
                            window.opener.vue.do_token(message);
                    }
                        window.close();
        }else{
            var state = {
                origin: searchs.origin,
                state: searchs.state
            };
            auth_location(searchs.client_id, searchs.scope, str2hex(JSON.stringify(state)));
        }
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );

function auth_location(client_id, scope, state){
    var params = {
        client_id: client_id,
        redirect_uri: REDIRECT_URL,
        response_type: 'code',
        state: state,
        scope: scope
    };
    window.location = COGNITO_URL + "/login" + to_urlparam(params);
}


function str2hex(str){
    return byteAry2hexStr(encoder.encode(str));
}

function hex2str(hex){
    return decoder.decode(new Uint8Array(hexStr2byteAry(hex)));   
}

function hexStr2byteAry(hexs, sep = '') {
    hexs = hexs.trim(hexs);
    if( sep == '' ){
        var array = [];
        for( var i = 0 ; i < hexs.length / 2 ; i++)
            array[i] = parseInt(hexs.substr(i * 2, 2), 16);
        return array;
    }else{
        return hexs.split(sep).map((h) => {
            return parseInt(h, 16);
        });
    }
}

function byteAry2hexStr(bytes, sep = '', pref = '') {
    if( bytes instanceof ArrayBuffer )
        bytes = new Uint8Array(bytes);
    if( bytes instanceof Uint8Array )
        bytes = Array.from(bytes);

    return bytes.map((b) => {
        var s = b.toString(16);
        return pref + (b < 0x10 ? '0'+s : s);
    })
    .join(sep);
}

function to_urlparam(qs){
    var params = new URLSearchParams();
    for( var key in qs )
        params.set(key, qs[key] );
    var param = params.toString();

    if( param == '' )
        return '';
    else
        return '?' + param;
}
vue_utils.js
var hashs = {};
var searchs = {};

function proc_load() {
  hashs = parse_url_vars(location.hash);
  searchs = parse_url_vars(location.search);
}

function parse_url_vars(param){
    var searchParams = new URLSearchParams(param);
    var vars = {};
    for (let p of searchParams)
        vars[p[0]] = p[1];

    return vars;
}

REDIRECT_URLは、自身のログイン専用ページのURLです。
COGNITO_URLは、Cognitoのエンドポイントです。

こんな感じにCognito User Poolに割り当てられているかと思います。
 https://<ドメイン名>.auth.ap-northeast-1.amazoncognito.com

大事なのは、

mounted: function(){

のところです。Vueを使っています。

最初にログイン専用ページがロードされたときには、まだ認可コードがないため、auth_location()を呼び出す分岐に入ります。

stateという変数を作っていますが、これは、Cognitoのエンドポイント呼び出し後に自身がリロードされ、元のページから取得していた情報を忘れてしまうため、Cognitoのエンドポイント呼び出し時のstateパラメータに埋め込んでしまっています。要は手抜きです。

auth_location() は、まさにCognitoのログインエンドポイントの呼び出しです。
呼び出しパラメータ等については、以下が参考になります。

 AWS Cognitoのエンドポイントを使いこなす

mountedは、Vueの規定で、画面ロード後に自動的に呼び出されます。
したがって、すぐに、Cognitoのログイン画面が表示されたかと思います。

例えばこんな感じです。実際には、Cognito User Poolに組み込んでいる認証プロバイダの種類によって見え方が異なります。

image.png

ログインが完了すると、REDIRECT_URLで指定したURL、すなわち、再度自身の画面がロードされます。
そのときに、認可コードが渡ってきていますので、今度は別の分岐に入ります。

その中で以下の分岐があります。

 if( state.origin ){

これは、ログイン専用ページと元のページが同じオリジンの処理とするか、異なるオリジンの処理とするかの分岐になります。
同じオリジンの場合には、元のページでparam.originをコメントアウトしていたかと思います。

■同じオリジンの場合

元のページのJavascript関数do_token()を直接呼び出しています。

■異なるオリジンの場合

JavascriptのPostMessage機能を使っています。

(参考情報) Window.postMessage
 https://developer.mozilla.org/ja/docs/Web/API/Window/postMessage

いずれも、認可コードと元のページから取得したstateパラメータを戻しています。
上記処理後、自身のログイン専用ページをcloseしています。

元のページに戻る

今一度元のページを見てみましょう。
同じオリジンの場合と異なるオリジンの場合で、ログイン専用ページからの戻りを取得する処理が異なります。

■同じオリジンの場合

do_tokenがログイン専用ページから直接呼び出されています。

start.js
const CLIENT_SECRET = 'アプリクライアントシークレット';
const COGNITO_URL = 'https://[ドメイン名].auth.ap-northeast-1.amazoncognito.com';

        do_token: function(message){
            if( this.state != message.state ){
                alert('state is mismatch');
                return;
            }
            var params = {
                grant_type: 'authorization_code',
                client_id: CLIENT_ID,
                redirect_uri: REDIRECT_URL,
                code: message.code
            };
            var url = COGNITO_URL + '/oauth2/token';
            return do_post_basic(url, params, CLIENT_ID, CLIENT_SECRET)
            .then(json =>{
                console.log(json);
                this.token = json;
            });
        }

渡されたstateが、ログイン専用ページ呼び出し時の値と同じであることを確認します。
そして、同じく渡された認可コードを使って、Cognitoのトークンエンドポイントを呼び出して、トークンを取得します。

※念のためですが、アプリクライアントシークレットをブラウザ上で処理してしまっています。漏洩しますので、本番ではサーバ側で秘匿に処理してください。

■異なるオリジンの場合

postMessageで送信された情報は、以下のところで取得されます。

start.js
window.addEventListener("message", (event) =>{
    console.log(event);
    if( event.origin != location.origin ){
        alert('origin mismatch');
        return;
    }
    vue.do_token(event.data);
}, false);

まずは、自身のオリジンと同じかどうかを確認します。param.originで指定したものとなっているはずです。
そして、同じオリジンの場合と同様、do_token()を呼び出します。

こんな感じで、認証およびトークン取得が成功し、画面に表示されるはずです。

image.png

以上

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

異体字の世界 【サイトウ】

異体字の世界 サイトウ

こんにちは。オープンロジの @ykhirao です。こちらは OPENLOGI Advent Calendar 2019 4日目の記事になります。

内容については、以前 pͪoͣnͬpͣoͥnͭpͣa͡inͥを支える技術 - Qiita という記事を書かせてもらったので、その続きでまた文字コードを扱った記事を書いていきたいと思います。どうぞ最後までよろしくお願いします!!

長いので結論

戸籍統一文字情報では 斎藤斉藤 は別の漢字として扱われているので、「サイトウさん、漢字はどのように書くのですか…?」と聞いたほうが無難な気がします。 ただ 斎藤 って書くと 62.2% 正解で、 斉藤 と書くと 30.3% 正解になります。

漢字 戸籍数(たぶん) %
斎藤 150494 62.2
斉藤 73424 30.3
齋藤 17071 7.1
齊藤 1111 0.5
合計: 242100 100

また法務省によると、旧自体と新字体が乗っていますので、

# 法務省
齋藤 → (新字体) → 斎藤
齊藤 → (新字体) → 斉藤

ここも考慮すると 斎藤 と書くと 69.3% 正解で、 斉藤 と書くと 30.7% 正解になります。

また子供のころに が難しかったので と書いていたケースも考えると、この二つの差は少し縮まりまるかもしれません。

とりあえず確立論的には と書けば…なんとなく、あたるかも…しれない?
(ちゃんと本人に確認とりましょう!!!)

拝啓 斎藤さん

先日エンジニアとしてサイトウさんが入社したときに「サイトウのサイってどう書きますか…? Unicodeで教えてください。」という、いつものあれが発生したのでそろそろサイトウについて理解してみようと思いました。

また以前に 『異体字の世界”ワタナベ”』| 漢検 漢字博物館・図書館 [漢字ミュージアム] というイベントがあったのでタイトルはそこから来ています。(展示にはいけてません…)

周辺知識編

Wikipedia

(ここから クリエイティブ・コモンズ CC-BY-SA 3.0 のライセンス)

Wikipediaで一つの項目となっているのは 斎藤斉藤 のみみたいです

この2つのWikipediaで重要なことは、 斎藤「斎宮頭を務めた藤原氏」の略 で、 についてはただの誤記説 (「斉」の文字に本来「さい」の読みがないことから斎藤の誤記が由来と言われる) が濃厚みたいです。

また法務省で定められている漢字としては、以下のように落ち着いているみたいです。

旧字体 新字体

(ここまで クリエイティブ・コモンズ CC-BY-SA 3.0 のライセンス)

ですので、現状 の新字体ではないので、小学生のときに「簡単な方」として書いていたケースを除いて、 2種類別の漢字として扱われているので正式な文章を書くときは2種類だけ気をつけたらよさそうです。

(ここから クリエイティブ・コモンズ CC-BY-SA 3.0 のライセンス)

また斎藤のページ によると、斎の派生は31種類あるみたいで、画像がはられています。

// 画像の注釈として
「斉藤」または「斎藤」の一文字目に使用される異体字はあわせて31種類あると言われている。

とありますが、それは後ほど出てくる 高信幸男氏 による調査による "西藤","西塔","才藤","済藤","西頭","西等","佐井藤","再藤" なども含んだ話なので、はられている画像と説明が食い違っているように思えます。が一旦は無視します。

また31種以外にも この他に例えば埼玉県の電話帳には齋の字の亠の下が刀丫氏の並びになっている齋藤姓が確認出来る。 との記載があり、どんどん派生しているのかもしれません。

(ここまで クリエイティブ・コモンズ CC-BY-SA 3.0 のライセンス)

TBSテレビ『この差って何ですか?』

また 「斎藤」「斉藤」「齋藤」「齊藤」・・この差って何? | 東洋経済オンライン名字研究家 高信幸男氏 からの情報がまとめられてますが、法務省が定めた見解とは少し違っていて

明治時代になると(略)国民全員に名字(略)多くの庶民が役所に行き、名字を申請した(略)

(中略)

役人はそれぞれ思い思いの「サイトウ」を書いてしまった(略)

具体的には「齋藤」は旧字体で書いてしまったパターンで、「齊藤」は旧字体の書き間違い。そして「斉藤」は本来の「斎藤」の書き間違い、というのだ。

とあるので、 の派生は としているところが面白いです。

# 法務省
齋 → 斎
齊 → 斉

# 高信幸男氏
齋 → (書き間違い) → 齊
↓
↓ (新字体)
↓
斎 → (書き間違い) → 斉

真実はいつも一つだと思いますが、文明開化の音とともに闇に消え去ったと思います。

また「サイトウ」と読む姓が31種類存在しているみたいで、人数の多い順に ["斎藤","斉藤","齋藤","齊藤","西藤","西塔","才藤","済藤","西頭","西等","佐井藤","再藤"] となっています。

三省堂 ことばのコラム

第78回 「斎」と「齋」 | 人名用漢字の新字旧字(安岡 孝一) | 三省堂 ことばのコラム によると の横棒の長さの揺れが出生届を出すときにすごく揺れていたみたいで、平成22年11月30日 に正式に 改定常用漢字表 で今の になったみたいです。結構最近ですね。

出来事 年代 漢字
標準漢字表 昭和17年6月17日 示
当用漢字表 昭和21年11月16日 小
戸籍法が改正 昭和23年1月1日 名付けに使用できるのは当用漢字表
漢字コード 規格
JIS C 6226
(コンピュータ表記)
昭和53年1月1日 示
常用漢字表 昭和56年10月1日 小
名付けに使用 (S56 ~ H22) 小
コンピュータ表記 (S56 ~ H22) 示
改定常用漢字表
コンピュータ表記と名付けでの表記が変わった
平成22年11月30日 示

漢字とは面白いですね。

調査と終えて今後の記事の目標

  • 斎という31字のユニコード特定

以上踏まえてコードなどを追っていきましょう。

Unicodeを特定する

Google 日本語入力から探す

サイトウ で変換できたそれっぽい字。(さんずいを除くと5字見つかった)

斎
斉
齋
齊
齎
済
濟
元の漢字 JS CODE UTF-16 コード(10進数) (16進数)
'斎'.charCodeAt(0) 25998 658E
'斉'.charCodeAt(0) 25993 6589
'齋'.charCodeAt(0) 40779 9F4B
'齊'.charCodeAt(0) 40778 9F4A
'齎'.charCodeAt(0) 40782 9F4E
'済'.charCodeAt(0) 28168 6E08
'濟'.charCodeAt(0) 28639 6FDF
// MACだと command + alt + i で開発ツールが開くのでConsoleなどで試してください。
$ ''.charCodeAt(0)
25998

$ String.fromCharCode(25998)
""

$ String.fromCharCode(0x658E)
""

10進法でいうと 2599840779 のあたりに サイ が固まってそうな雰囲気がしますね。

Unicodeと漢字をマッピングしてくれるサイトを探す

https://glyphwiki.org/wiki/u658e

グリフウィキに登録されているグリフデータおよび記事は、誰もが自由に利用できることとします。あらゆる改変の有無に関わらず、また商業的な利用であっても、自由に利用、複製、再配布することができます。著作者表示も特に制限を設けません。新しいフォントのベースデータとして用いることや、そのままコピーしたものをフォントとして著作物とすることを妨げません。記事中に引用されている部分については、グリフウィキには著作権はありませんので引用元のライセンスを確認してください。

https://glyphwiki.org/wiki/GlyphWiki:データ・記事のライセンス

とても使いやすそうなライセンスなのでこのページをメインで調べてみようと思う。

グリフウィキを広めてください
登録されているグリフの画像やフォントをどんどん利用してください。画像やフォントに直接リンクを張ってもかまいません。またグリフウィキの存在をクチコミでみんなに広めてください。

https://glyphwiki.org/wiki/GlyphWiki:あなたにできること

またグリフウィキさんを広めるのは正義みたいなので、、私の代わりに使ってくれ。画像への直リンク可みたいなので使うことにする。

先程の へのリンクを確認する限り、Unicodeとして定義されているのは、さきほどあげた5種類の サイ だけなのではないかと思い初めてきた…。その他は 法務省 が規定する戸籍関連の字とか、中国語字体とか…なんかそういうのも含めている気がする。

の異体字・関連文字データ抽出する

https://glyphwiki.org/wiki/u658e

// データ作成
const td = document.querySelector('body > div.right_pane > div.right_body > div:nth-child(15) > table > tbody > tr:nth-child(2) > td:nth-child(3)')
const arr = [...td.querySelectorAll('a>img')]
arr.map(x => `${x.parentNode.href}\t${x.parentNode.innerText.trim()}`).join('\n')
arr.map(x => x.src).join('\n')

斎 u658e の異体字は32個登録されていて、右月になっている u9f4b-02 の系統の u9f4b-02 u9f4b-02-var-001 u9f4b-02-var-002 u9f4b-02-var-003 の4つを除き、 斎 u658e の一文字足すと、29個確認された。

コード サイトリンク 画像
u658e https://glyphwiki.org/wiki/u658e
u9f4b https://glyphwiki.org/wiki/u9f4b
u9f4b-02 https://glyphwiki.org/wiki/u9f4b-02
u9f4b-02-var-001 https://glyphwiki.org/wiki/u9f4b-02-var-001
u9f4b-02-var-002 https://glyphwiki.org/wiki/u9f4b-02-var-002
u9f4b-02-var-003 https://glyphwiki.org/wiki/u9f4b-02-var-003
u9f4b-itaiji-001 https://glyphwiki.org/wiki/u9f4b-itaiji-001
u9f4b-j https://glyphwiki.org/wiki/u9f4b-j
u9f4b-ue0101 https://glyphwiki.org/wiki/u9f4b-ue0101
u9f4b-var-001 https://glyphwiki.org/wiki/u9f4b-var-001
u9f4b-var-002 https://glyphwiki.org/wiki/u9f4b-var-002
u9f4b-var-003 https://glyphwiki.org/wiki/u9f4b-var-003
u9f4b-var-004 https://glyphwiki.org/wiki/u9f4b-var-004
u9f4b-var-005 https://glyphwiki.org/wiki/u9f4b-var-005
cbeta-30389 https://glyphwiki.org/wiki/cbeta-30389
cbeta-31099 https://glyphwiki.org/wiki/cbeta-31099
jmj-059306 https://glyphwiki.org/wiki/jmj-059306
juki-ad38 https://glyphwiki.org/wiki/juki-ad38
juki-ad39 https://glyphwiki.org/wiki/juki-ad39
juki-b720 https://glyphwiki.org/wiki/juki-b720
koseki-548960 https://glyphwiki.org/wiki/koseki-548960
toki-01001950 https://glyphwiki.org/wiki/toki-01001950
toki-01066230 https://glyphwiki.org/wiki/toki-01066230
u2ff5-u2ff3-u4ea0-u6c36-cdp-89c6-u5c0f https://glyphwiki.org/wiki/u2ff5-u2ff3-u4ea0-u6c36-cdp-89c6-u5c0f
zihai-014012 https://glyphwiki.org/wiki/zihai-014012
zihai-014103 https://glyphwiki.org/wiki/zihai-014103
zihai-014137 https://glyphwiki.org/wiki/zihai-014137
zihai-014217 https://glyphwiki.org/wiki/zihai-014217
zihai-014218 https://glyphwiki.org/wiki/zihai-014218
zihai-014223 https://glyphwiki.org/wiki/zihai-014223
zihai-020358 https://glyphwiki.org/wiki/zihai-020358
hkcs_m9f4b https://glyphwiki.org/wiki/hkcs_m9f4b
hkcs_u9f4b https://glyphwiki.org/wiki/hkcs_u9f4b

そして残念ながらこれらの字体は、ほとんどがUnicodeには登録されていなくて

[異体字(常用漢字表)]
[異体字(戸籍統一文字)]
[異體字(民國教育部)]
[異體字(漢語大字典)]
[関連字(JIS X 0212)]

などに定義されているみたいである。

koseki-548960 となっている文字の正確な情報は 法務省の戸籍統一文字情報 http://kosekimoji.moj.go.jp/kosekimojidb/mjko/PeopleTop/EXECUTE から探すことになる。

もしくは後術する MJ文字情報検索システム | 独立行政法人 情報処理推進機構 - IPA から探す。

の異体字・関連文字データを抽出する

とりあえず関連データを引っ張ってきた。少なかったのといろいろあって手作業。

コード サイトリンク 画像
u6589 https://glyphwiki.org/wiki/u6589
u658a https://glyphwiki.org/wiki/u658a
u4e9d https://glyphwiki.org/wiki/u4e9d
u9f4a https://glyphwiki.org/wiki/u9f4a
u2bfed https://glyphwiki.org/wiki/u2bfed
u2d918 https://glyphwiki.org/wiki/u2d918
u2ebbc https://glyphwiki.org/wiki/u2ebbc

unicodeにありそうな上4つを見てみた。下3つは桁が一つ多い…?

String.fromCharCode(0x6589)
""
String.fromCharCode(0x658a)
""
String.fromCharCode(0x4e9d)
""
String.fromCharCode(0x9f4a)
""
String.fromCharCode(0x2bfed)

MJ文字情報検索システム | 独立行政法人 情報処理推進機構 - IPA

https://mojikiban.ipa.go.jp/search/code

良い感じのサイトを見つけた。

Screenshot from 2019-11-29 18-58-02.png

IPAが文字情報を統一的に調べられるようにしている。信頼できそう。

なんとなく細かいところを比べてみる

斎 の異体字・関連文字データ抽出する で抽出したデータの細かいところを見ていきましょう。

コード 画像 真ん中
u9f4b ①普通の鍋蓋 Y ①氏みたいなやつ ① 示で上が左右くっついている、下はねなし
u9f4b-itaiji-001 ②ちょんと鍋蓋 ② 普通の示、下はねあり
u9f4b-j Y
u9f4b-ue0101
u9f4b-var-001 Y ⑤ ①の下跳ねあり版
u9f4b-var-002 Y ③ ①の示すに見せかけて右側がくっついてない
u9f4b-var-003 Y ④ ③の小の下がはねてない
u9f4b-var-004 Y
u9f4b-var-005 Y ⑥ ①で上の右側だけくっついてない
jmj-059306
juki-ad38 ②氏のパート2
juki-ad39
juki-b720 Y ⑦ ②の下はねなし版
koseki-548960
toki-01001950
toki-01066230 Y
u2ff5-u2ff3-u4ea0-u6c36-cdp-89c6-u5c0f 水と永の間 水と永の間 水と永の間
zihai-014137 Y ⑧ 丙みたいなやつ
zihai-014217 Y ⑨ 米
zihai-014218 Y
zihai-014223
hkcs_m9f4b Y
hkcs_u9f4b Y ⑩ ①と②の中間くらい

作っている途中(23:11)で力ついて途中適当になりました。なんか雰囲気たくさん種類あるんだなーーーくらいに思っておいて、正確さは求めないでください。

まとめ

長い記事を読んでいただきありがとうございました!!!

サイトウは誤字から生まれたのはちょっとおもしろかったです。w

お疲れ様でした。!!!


.

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

Javascript で diff (内部仕様についての補足7)

10/14 に投稿したJavascript で diff (通信なし、ローカルで完結)の内部仕様についての補足、その7です。
Javascript で diff (内部仕様についての補足)
Javascript で diff (内部仕様についての補足2)
Javascript で diff (内部仕様についての補足3)
Javascript で diff (内部仕様についての補足4)
Javascript で diff (内部仕様についての補足5)
Javascript で diff (内部仕様についての補足6)

補足1にあった高速化の話をこちらに独立させて、補足1のほうにアルゴリズムの説明を加筆しました。

高速化を試みた

実際に実装してみて、表示結果は希望通りになったのですが、レーベンシュタイン距離の計算がとにかく遅い!!
10000行程度のファイルどうしを比較すると数分以上待たされてしまったので、何とか高速化することにします。
行どうしの類似度は、文字列どうしが似ているかどうかだけ分かればよいので、似ていないと分かった時点でスコア計算を打ち切るようにしました。
また、ファイル内でユニークな内容を持つ行(=同じ内容の行は他に存在しない)について、比較対象のファイルにも同じ内容でユニークな行が存在すれば、そこで行どうしのペアが確定するため、レーベンシュタイン距離の計算対象から外します。行のハッシュ値を生成した直後の処理で、ユニークな行どうしでペアが作れるかを調べて記録しています。

JavaScript で関数を入れ子にするとものすごく遅かったので、インライン展開できるところはなるべくインライン展開するようにしています。内側の関数から外側の変数を参照すると遅いようです。

あとはループ自体の最適化、計算結果をキャッシュできるところはなるべくキャッシュする、などの地味な最適化を行っています。
実際に両方試さないとわからないところは、両方実装して実験しました。(配列の処理をmapで回すのとforで回すのとの比較とか)
いわゆるsnake関数のメインループにあたるところは、教科書にあるような説明的なループからカリカリにチューンナップしたので、原形はかろうじてとどめている感じになりました。

Diff_(len1, len2, bReverse) {
    const bStrict = !this.bDetectSimilarLine, threshold = this.threshold, _max = Math.max;
    let n = len1 + len2 + 3, fp = (new Int32Array(n)).fill(-1), ed = (new Array(n)).fill(null);
    for (let p = 0; fp[len2 + 1] != len2; p++) {
        for (let k = len1 - p; k < len2; k++) {
            let y = _max(fp[k] + 1, fp[k + 2]), x = y - k + len1, bIsBlank, y0, org, score;
            let [x1, y1, k_plus, k_minus] = (bReverse ? [y, x, k + 2, k] : [x, y, k, k + 2]);
            if (bReverse ? (y == fp[k + 2]) : ((y == (fp[k] + 1)) && (fp[k + 2] != 0))) {
                if (y1 > 0) { ed[k + 1] = {op:'+', x:-1, y:y1 - 1, prev:ed[k_plus]}; }
            } else {
                if (x1 > 0) { ed[k + 1] = {op:'-', x:x1 - 1, y:-1, prev:ed[k_minus]}; }
            }
            for (bIsBlank = true, y0 = y, org = ed[k + 1]; x < len1; x1++, y1++, x++, y++) {
                if ((score = this.CompareStr(x1, y1, bStrict)) <= threshold) { break; }
                if (!this.vInfo_L[x1].isBlank) { bIsBlank = false; }
                ed[k + 1] = {op:((score > 1) ? '=' : '*'), x:x1, y:y1, prev:ed[k + 1]};
            }
            if (bIsBlank && !((x >= (len1 - 1)) && (y >= (len2 - 1)))) { [y, ed[k + 1]] = [y0, org]; }
            fp[k + 1] = y;
        }
        for (let k = len2 + p; k >= len2; k--) {
            let y = _max(fp[k] + 1, fp[k + 2]), x = y - k + len1, bIsBlank, y0, org, score;
            let [x1, y1, k_plus, k_minus] = (bReverse ? [y, x, k + 2, k] : [x, y, k, k + 2]);
            if (bReverse ? (y == fp[k + 2]) : ((y == (fp[k] + 1)) && (fp[k + 2] != 0))) {
                if (y1 > 0) { ed[k + 1] = {op:'+', x:-1, y:y1 - 1, prev:ed[k_plus]}; }
            } else {
                if (x1 > 0) { ed[k + 1] = {op:'-', x:x1 - 1, y:-1, prev:ed[k_minus]}; }
            }
            for (bIsBlank = true, y0 = y, org = ed[k + 1]; y < len2; x1++, y1++, x++, y++) {
                if ((score = this.CompareStr(x1, y1, bStrict)) <= threshold) { break; }
                if (!this.vInfo_L[x1].isBlank) { bIsBlank = false; }
                ed[k + 1] = {op:((score > 1) ? '=' : '*'), x:x1, y:y1, prev:ed[k + 1]};
            }
            if (bIsBlank && !((x >= (len1 - 1)) && (y >= (len2 - 1)))) { [y, ed[k + 1]] = [y0, org]; }
            fp[k + 1] = y;
        }
    }
    return ed[len2 + 1];
},

なんとか、10000行くらいのファイルを数秒から10秒くらいで比較できるようになったのですが、基本アルゴリズムがO(N^2)なので、リアルタイムで表示を更新するのはあきらめました。(このへんがdiff_orzの名前の由来だったり………)

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

Uint8ClampedArrayに小数を代入すると詰まる

先に結論だけ

Uint8ClampedArrayに小数を代入すると、0.5以下が切り捨てられます。複合代入演算子を利用しても同様です。

この記事の想定する環境

この記事は以下の環境で動作することを想定しています。

  • Google Chrome 78.0.3904.108

記事を読む前に、お手元の環境をご確認ください。

Uint8ClampedArrayとは

Uint8ClampedArrayは、8ビット符号なし整数値の配列です。JavaScriptのNumberは、浮動小数点を扱いますがこの配列は0 ~ 255の整数値に値が制限されます。Uint8ClampedArrayなどのTypedArrayは型付きの要素の配列です。

どのような操作で必要となるのか

Canvas要素のImageDataオブジェクトが、ピクセルデータをUint8ClampedArrayで格納しています。TypedArrayは一般的に、バイナリデータをJavaScriptで操作する際のインターフェイスです。

小数を代入してみる

このUint8ClampedArrayに、小数を代入するとどのような振る舞いをするでしょう?情報がなかったため、Codepenでテストコードを書いてみました。

const array = new Uint8ClampedArray(1);

for( let i = 1; i < 101; i++){
  const val =  1.0 - (i / 100)
  array[0] = val;
  console.log( val , array[0] )
}

このコードの出力は以下の通りです。

0.99 1
pen.js:7 0.98 1
pen.js:7 0.97 1
...(中略)...
pen.js:7 0.51 1
pen.js:7 0.5 0
pen.js:7 0.49 0
...(中略)...
pen.js:7 0.020000000000000018 0
pen.js:7 0.010000000000000009 0
pen.js:7 0 0

0.5以下が切り捨て、それ以上では1に丸め込みが行われていました。四捨五入ではなく0.5で切り捨てが行われているのは意外でした。

どんなときに困るのか

JavaScriptのNumber型は浮動小数点なので、うっかり以下のようなコードを書いてしまうとバグを生みます。

array[0] *= 0.8;

このコードではarray[0]が2以下になると、数値がそれ以上減りません。丸め込み処理が行われるためです。例としてCanvasのピクセルを徐々に暗くしたいのに、いつまでもわずかな灰色が残ります。

array[0] = ( array[0] * 0.8 ) | 0;

Math.floorやビット演算で明示的に小数を切り捨てると、この問題は解決します。

参考記事 : HTML5 canvas のパフォーマンスの改善 / 浮動小数点座標の使用を控える

整数型を持った言語では当然問題になるコードですが、JavaScriptでも同様の注意が必要となる場合があります。
特にCanvasを操作する場合はご注意ください。

以上、ありがとうございました。

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

フォームのJavaScript

フォームでJavaScriptで何かするときの参考用です。
フォームのCSSも併せてご参照ください。

フォーカス

フォーカス状態の変更イベント

html
<form name="contactForm">
  <input name="focusTest" />
</form>
js
const focusTest = document.contactForm.focusTest;

focusTest.addEventListener("focus", () => {
  console.log("フォーカスされた");
});

focusTest.addEventListener("blur", () => {
  console.log("フォーカスが外れた");
});

input [type="text"]

値の取得

html
<form name="contactForm">
  <input name="inputText" type="text" value="こんにちは" />
</form>
js
const inputText = document.contactForm.inputText;

console.log(inputText.value);

値の変更

html
<form name="contactForm">
  <input name="inputText" type="text" value="こんにちは" />
</form>
js
const inputText = document.contactForm.inputText;

inputText.value = "こんばんは";

値の変更イベント

イベント タイミング
change 値変更されて、フォーカスが外れた時、Enterが押された時
input キー入力時
html
<form name="contactForm">
  <input name="inputText" type="text" value="こんにちは" />
</form>
js
const inputText = document.contactForm.inputText;

inputText.addEventListener("change", () => {
  console.log("値が変更された");
})

inputText.addEventListener("input", () => {
  console.log("キーが押された");
})

input[type="range"]

値の取得

html
<form name="contactForm">
  <input name="inputRange" type="range">
 /form>
js
const inputRange = document.contactForm.inputRange;

console.log(inputRange.value);

値の変更

html
<form name="contactForm">
  <input name="inputRange" type="range">
</form>
js
const inputRange = document.contactForm.inputRange;

inputRange.value = 100;

値の変更イベント

イベント タイミング
change 止まってから
input 動くたび
html
<form name="contactForm">
  <input name="inputRange" type="range">
</form>
js
const inputRange = document.contactForm.inputRange;

inputRange.addEventListener("change", () => {
  console.log(inputRange.value);
});

input[type="color"]

値の取得

html
<form name="contactForm">
  <input name="inputColor" type="color">
</form>
js
const inputColor = document.contactForm.inputColor;

console.log(inputColor.value);

値の変更

html
<form name="contactForm">
  <input name="inputColor" type="color">
</form>
js
const inputColor = document.contactForm.inputColor;

inputColor.value = "#ffffff";

値の変更イベント

html
<form name="contactForm">
  <input name="inputColor" type="color">
</form>
js
const inputColor = document.contactForm.inputColor;

inputColor.addEventListener("change", () => {
  console.log(inputColor.value);
})

input[type="file"]

ファイル情報の取得

html
<form name="contactForm">
  <input name="inputfile" type="file" multiple>
</form>
js
const inputfile = document.contactForm.inputfile;

inputfile.addEventListener("change", () => {

  for (let i = 0; i < event.target.files.length; i++) {

    console.log(`ファイル名:${event.target.files[i].name}`);
    console.log(`ファイルサイズ:${event.target.files[i].size}`);
    console.log(`ファイルタイプ:${event.target.files[i].type}`);
    console.log(`ファイル最終更新日時:${event.target.files[i].lastModifiedDate}`);
    console.log(`ファイル最終更新日時(UNIXタイムスタンプ):${event.target.files[i].lastModified}`);

  }

});

テキストファイルの内容を読み込む

html
<form name="contactForm">
  <input name="inputfile" type="file" multiple accept=".txt">
</form>
js
const inputfile = document.contactForm.inputfile;

inputfile.addEventListener("change", () => {

  for (let i = 0; i < event.target.files.length; i++) {

    console.log(`ファイル名:${event.target.files[i].name}`);
    console.log(`ファイルサイズ:${event.target.files[i].size}`);
    console.log(`ファイルタイプ:${event.target.files[i].type}`);
    console.log(`ファイル最終更新日時:${event.target.files[i].lastModifiedDate}`);
    console.log(`ファイル最終更新日時(UNIXタイムスタンプ):${event.target.files[i].lastModified}`);

    const txtReader = new FileReader();

    txtReader.addEventListener("load", () => {
       console.log(txtReader.result);
    });

    txtReader.readAsText(event.target.files[i]);

  }

});

画像をDataURLとして読み込んで表示

html
<form name="contactForm">
  <input name="inputfile" type="file" multiple accept=".jpg,.png,.gif,.svg">
</form>
<div id="js-imgArea"></div>
js
const inputfile = document.contactForm.inputfile;
const imgArea = document.querySelector("#js-imgArea");

inputfile.addEventListener("change", () => {

  for (let i = 0; i < event.target.files.length; i++) {

    console.log(`ファイル名:${event.target.files[i].name}`);
    console.log(`ファイルサイズ:${event.target.files[i].size}`);
    console.log(`ファイルタイプ:${event.target.files[i].type}`);
    console.log(`ファイル最終更新日時:${event.target.files[i].lastModifiedDate}`);
    console.log(`ファイル最終更新日時(UNIXタイムスタンプ):${event.target.files[i].lastModified}`);

    const imgReader = new FileReader();

    imgReader.addEventListener("load", () => {
      const img = document.createElement("img");
      img.src = imgReader.result;
      imgArea.appendChild(img);
    });

    imgReader.readAsDataURL(event.target.files[i]);

  }

});

ラジオボタン

値の取得

html
<form name="contactForm">
  <input type="radio" name="radioGroup" id="form__input--radio1" value="a" checked>
  <label for="form__input--radio1">a</label>
  <input type="radio" name="radioGroup" id="form__input--radio2" value="b">
  <label for="form__input--radio2">b</label>
</form>
js
const radioGroup = document.contactForm.radioGroup;

console.log(`${radioGroup.value}が選択されている`);

値の変更

html
<form name="contactForm">
  <input type="radio" name="radioGroup" id="form__input--radio1" value="a" checked>
  <label for="form__input--radio1">a</label>
  <input type="radio" name="radioGroup" id="form__input--radio2" value="b">
  <label for="form__input--radio2">b</label>
</form>
js
const radioGroup = document.contactForm.radioGroup;

radioGroup.value = "b";

値の変更イベントを取得

html
<form name="contactForm">
  <input type="radio" name="radioGroup" id="form__input--radio1" value="a">
  <label for="form__input--radio1">a</label>
  <input type="radio" name="radioGroup" id="form__input--radio2" value="b">
  <label for="form__input--radio2">b</label>
</form>
js
const form = document.contactForm;

form.addEventListener("change", () => {
  console.log(`${form.radioGroup.value}が選択された`);
});

チェックボックス

値の取得

html
<form name="contactForm">
  <input name="checkbox1" type="checkbox" checked>
  <input name="checkbox2" type="checkbox">
</form>
js
const checkbox1 = document.contactForm.checkbox1;
console.log(checkbox1.checked);

const checkbox2 = document.contactForm.checkbox2;
console.log(checkbox2.checked);

値の変更

html
<form name="contactForm">
  <input name="checkbox" type="checkbox">
</form>
js
const checkbox = document.contactForm.checkbox;

checkbox.checked = true;

値の変更イベント

イベント タイミング
change 値が変更されて、フォーカスが外れた時
html
<form name="contactForm">
  <input name="checkbox" type="checkbox">
</form>
js
const checkbox = document.contactForm.checkbox;

checkbox.addEventListener("change", () => {
  console.log("チェックボックスの状態が変更された");

  if (checkbox.checked === false) {
    console.log("チェックが外れた");
  }

  if (checkbox.checked === true) {
    console.log("チェックされた");
  }

});

セレクトボックス

値の取得

html
<form name="contactForm">
  <select name="select">
    <option value="a">a</option>
    <option value="b" selected>b</option>
    <option value="c">c</option>
  </select>
</form>
js
const select = document.contactForm.select;

console.log(select.value);

値の変更

html
<form name="contactForm">
  <select name="select">
    <option value="a">a</option>
    <option value="b" selected>b</option>
    <option value="c">c</option>
  </select>
</form>
js
const select = document.contactForm.select;

select.value = "c";

値の変更イベント

html
<form name="contactForm">
  <select name="select">
    <option value="a">a</option>
    <option value="b" selected>b</option>
    <option value="c">c</option>
  </select>
</form>
js
const select = document.contactForm.select;

select.addEventListener("change", () => {
  console.log(`${select.value}が選択された`);
})

textarea

値の取得

html
<form name="contactForm">
  <textarea name="textarea">こんにちは</textarea>
</form>
js
const textarea = document.contactForm.textarea;

console.log(textarea.value);

値の変更

html
<form name="contactForm">
  <textarea name="textarea">こんにちは</textarea>
</form>
js
const textarea = document.contactForm.textarea;

textarea.value = "こんばんは";

値の変更イベント

イベント タイミング
change 値変更されて、フォーカスが外れた時
input キー入力時
html
<form name="contactForm">
  <textarea name="textarea">こんにちは</textarea>
</form>
js
const textarea = document.contactForm.textarea;

textarea.addEventListener("change", () => {
  console.log("値が変更された");
});

textarea.addEventListener("input", () => {
  console.log("キーが押された");
});

submit

フォームの送信前イベント

html
<form name="contactForm" action="thanks.php" method="post">
  <button type="submit">送信</button>
</form>
js
const form = document.contactForm;

  form.addEventListener("submit", () => {

  const confirmFlag = confirm("送信OK?");
  console.log(confirmFlag);

  if (confirmFlag === false) {
    event.preventDefault();
  }

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

2019の終わりにES2019を覚えておく

ES2015が世の中に浸透し始めたときには、必死こいて「このビッグウェーブに乗らなきゃ!」と人並みに勉強しましたが、それ以降の動向は追っていませんでした。ここのところ、年末ということもあってかES2019についての記事をちらほら見ることが増えて、今年のことは今年のうちに覚えておこう、という動機です。ES2016〜ES2018についても理解していませんが、それは来年まとめてやっつけます。

ES2019には何が含まれているのか、この互換性テーブルを見ると分かりやすいです。

互換性テーブルを元にES2019に追加された機能は次の通りです。

2019 features

  • Object.fromEntries
  • string trimming
  • Array.prototype.{flat, flatMap}

2019 misc

  • optional catch binding
  • Symbol.prototype.description
  • Function.prototype.toString revision
  • JSON superset
  • Well-formed JSON.stringify

今回は、この中で2019 featuresの3つ、2019 miscの中から覚えておくと良さそうな optional catch binding をピックアップして勉強します。

それではひとつひとつ何をするためのものなのか、ざっくりと見ていきます。

※本記事は理解力、読解力ともに難がある私自身が理解するために、かなり噛み砕いた文章になるので、文字を読むのが得意な方は互換性テーブルから各機能のリンク先を読むことをオススメします。

Object.fromEntries

Object.fromEntries() メソッドは、キーと値の組み合わせのリストをオブジェクトに変換します。
MDN:Object.fromEntries

const fruits = Object.fromEntries([['banana', 2], ['apple', 3]])
// {banana: 2, apple: 3}

参考:Object.fromEntries

string trimming

互換性テーブルを参考に、次の4つの機能について見ていきます。

  • String.prototype.trimLeft
  • String.prototype.trimRight
  • String.prototype.trimStart
  • String.prototype.trimEnd

String.prototype.trimLeftString.prototype.trimStart のエイリアス、String.prototype.trimRight は、 String.prototype.trimEnd のエイリアスなので、実質以下2つを覚えれば良いと解釈します。

  • String.prototype.trimStart
  • String.prototype.trimEnd

String.prototype.trimStart

先頭の空白を削除します。

const sample = '     こんにちは、世界よ     ';
console.log(sample.trimStart());
// 'こんにちは、世界よ     '

参考:String.prototype.trimStart

String.prototype.trimEnd

末尾の空白を削除します。

const sample = '     こんにちは、世界よ     ';
console.log(sample.trimEnd());
// '     こんにちは、世界よ'

参考:String.prototype.trimEnd

Array.prototype.{flat, flatMap}

Array.prototype.flat

ネストされた配列をフラットにします。

const arr1 = [1, 2, [3, 4]];
// [1, 2, 3, 4]

参考:Array.prototype.flat

Array.prototype.flatMap

map したものをフラットにしたい場合、Array.prototype.flatMap を使わないと、まず map してから flat 関数を使ってフラットにします。
しかし、 Array.prototype.flatMap を使うと、一つの関数でフラットにできます。

let arr1 = [1, 2, 3, 4];

const arr2 = arr1.map(x => [x * 2]).flat(); 
// [2, 4, 6, 8]

const arr3 = arr1.flatMap(x => [x * 2]);
// [2, 4, 6, 8]

参考:Array.prototype.flatMap

optional catch binding

バインディングが使用されない場合にキャッチバインディングを省略できる、というものです。
つまり、try-catchは通常次のようになりますが、

try {
} catch(e) {
}

バインディングを使用しない場合は、記述を省略できるという文法の変更です。

try {
} catch {
}

参考:optional catch binding

終わりに

ES2019だけにフォーカスすれば量としてはそんなに多くなかったのですが、勉強していなかったES2016〜2018をまとめてインプットするのは中々ヘビーだなという印象です。次回はES2016にフォーカスして勉強してみようと思います。

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

【JavaScript】 配列の値渡しは結局どれを使えばいいのか... (速度比較)

最速でコピーしたい...

最近AtCoderにハマってて配列のコピー結局どれ使えばいいの??
ってなったので適当すぎるコードで適当に速度調べてみました。

今回比較する4種類のコピー

テストコード

以下クソコード

const a=[1,2,3,4,5,6,7,8,9]
let b
for(let i=0;i<10000000;i++){
  // 1
  b=[...a]
  // 2
  b=Array.from(a)
  // 3
  b=a.concat()
  // 4
  b=a.slice()
}

結果

spred(...) from concat slice
1088ms error 1400ms 1077ms

[spread]=[slice]>[concat]>>>>>>>>>>>>>[from]
って感じですね。
ちなみにメモリはどれも似たような感じでした。
fromはループ数減らすと動きました。
遅すぎてerrorはいてるっぽいです。
結局今まで通りスプレッド構文使おうかなと思いました。

ところがどっこい 「スプレッドよりsliceの方が早い!?」

nagtkk様より頂きました情報によりますと

競プロではなくブラウザ関係で時折測定していますけれど、
最近はどの環境も slice が頭一つ抜けて最速になることが多いですね。
ちなみに V8 に関しては、何故か spread よりも destructuring + rest の方が速かったり。

let b = [...a];
// ↓
let [...b] = a; // 倍以上速くなることもしばしば。

まあ、slice の方が速いので、そっちでいいじゃんってなるんですが...。

実は何度かテストしてて若干sliceの方が早いんじゃね?
とは思ってたのですが環境次第ではかなり差が出ることもあるようです!

結論

slice使いましょう XD

終わり

JavaScriptで競プロは辛いのでやめましょう:(

間違っていることあればご指摘お願いいたします。

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

Pulumiを使ってEKSを作成してみた。

JavaScriptやTypeScript,Python,C#などの高級言語でInfra as Codeを書けるPulumiを使って、AWSのEKSの構築スクリプトを書いてみました。
TypeScriptでコード補完を使ってコードが書けるので、Infra as Codeを書くのが楽ですね。

EKSの構成は、Terraformを使ってEKSを作成してみたを参考に作りました。コードはGitHubに置いています。

コードの書き方の流れ

以下の流れでコードを書きました。

  1. IAM
  2. VPC
  3. SecurityGroup
  4. EKS
  5. Auto Scaling Group
  6. Kubernetes の疎通用サンプル

共通的に利用可能な関数は src/func/**x.ts ファイルに共通関数を切り出しています。

1. IAM

EKSではMasterNode用のIAMとWorkerNode用のIAMが必要になります。
それぞれのIAMの設定のコードを 共通関数として切り出し、まとめました。

src/func/iamx.ts
import * as awsx from "@pulumi/awsx";
import * as aws from "@pulumi/aws";

export function createEksMasterRole(name: string) {
  const eksMasterRole = new aws.iam.Role(name,{
    assumeRolePolicy:{
      "Version": "2012-10-17",
      "Statement": [{
        "Action": "sts:AssumeRole",
        "Principal": {
          "Service": "eks.amazonaws.com"
        },
        "Effect": "Allow"
      }
    ]
  }
  });

  const amazonEKSClusterPolicy = new aws.iam.PolicyAttachment(name +"/AmazonEKSClusterPolicy", {
    policyArn: "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy",
    roles: [eksMasterRole]
  });

  const amazonEKSServicePolicy = new aws.iam.PolicyAttachment(name + "/AmazonEKSServicePolicy", {
    policyArn: "arn:aws:iam::aws:policy/AmazonEKSServicePolicy",
    roles: [eksMasterRole]
  });

  return {
    role: eksMasterRole,
    amazonEKSClusterPolicy: amazonEKSClusterPolicy,
    amazonEKSServicePolicy: amazonEKSServicePolicy 
  };
}

export function createEksWorkerRole(name:string){
  const eksWorkerRole =  new aws.iam.Role(name,{
    assumeRolePolicy:{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "sts:AssumeRole",
        "Principal": {
          "Service": "ec2.amazonaws.com"
        },
        "Effect": "Allow"
      }
    ] 
    }
  });

  const eKSWorkerNodePolicy = new aws.iam.PolicyAttachment(name + "/EKSWorkerNodePolicy",{
    policyArn:"arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy",
    roles:[eksWorkerRole]
  });

  const amazonEKS_CNI_Policy = new aws.iam.PolicyAttachment(name + "/AmazonEKS_CNI_Policy",{
    policyArn:"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy",
    roles:[eksWorkerRole]
  });

  const amazonEC2ContainerRegistryReadOnly = new aws.iam.PolicyAttachment(name + "/AmazonEC2ContainerRegistryReadOnly",{
    policyArn:"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
    roles:[eksWorkerRole]
  });

  const amazonEC2RoleforSSM = new aws.iam.PolicyAttachment(name + "/AmazonEC2RoleforSSM",{
    policyArn:"arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM",
    roles:[eksWorkerRole]
  });

  const instanceProfile = new aws.iam.InstanceProfile(name + "/InstanceProfile",{
    name : "eks-node-role-profile",
    role : eksWorkerRole
  })

  return {
    role:eksWorkerRole,
    eKSWorkerNodePolicy:eKSWorkerNodePolicy,
    amazonEKS_CNI_Policy:amazonEKS_CNI_Policy,
    amazonEC2ContainerRegistryReadOnly:amazonEC2ContainerRegistryReadOnly,
    amazonEC2RoleforSSM:amazonEC2RoleforSSM,
    instanceProfile:instanceProfile
  }

}

作成した IAM の 共通関数を呼び出して、登録します。

src/1.iam.ts
import * as aws from "@pulumi/aws";
import * as iamx from "./func/iamx";

export const eksMasterRole =iamx.createEksMasterRole("eks-master-role");
export const eksWorkerRole = iamx.createEksWorkerRole("eks-worker-role");

2. VPC

VPCはパブリックネットワークのサブネットの作成が繰り返しの処理になっていたため、サブネット作成を共通関数として切り出しました。

src/func/vpcx.ts
import * as aws from "@pulumi/aws";

export function generateSubnet(
  name: string,
  args: {
    cidrBlock: string;
    vpc: aws.ec2.Vpc;
    az: string;
    igw: aws.ec2.InternetGateway;
    routeTable:aws.ec2.RouteTable
  }
){
  const subnet = new aws.ec2.Subnet(name, {
    cidrBlock: args.cidrBlock,
    vpcId: args.vpc.id,
    availabilityZone: args.az,
    mapPublicIpOnLaunch: true
  });

  const routeTableAssociation = new aws.ec2.RouteTableAssociation(
    name + "-rtba",
    {
      subnetId: subnet.id,
      routeTableId: args.routeTable.id
    }
  );

  return subnet;
};

次に、Terraformのコードを参考に3つのAZで構成されるサブネットを持つVPCの設定をコーディングしました。

src/2.vpc.ts
import * as aws from "@pulumi/aws";
import * as vpcx from "./func/vpcx";

export const vpc = new aws.ec2.Vpc("eks-vpc", {
  cidrBlock: "10.0.0.0/16",
  enableDnsHostnames: true,
  enableDnsSupport: true,
  tags: { Name: "eks-vpc" }
});

export const igw = new aws.ec2.InternetGateway("igw", {
  vpcId: vpc.id,
  tags: {
    Name: "eks-igw"
  }
});

export const routeTable = new aws.ec2.RouteTable("rtb", {
  routes: [
    {
      cidrBlock: "0.0.0.0/0",
      gatewayId: igw.id
    }
  ],
  tags: {
    Name: "eks-rtb"
  },
  vpcId: vpc.id
});

export const subnets = [
  vpcx.generateSubnet("eks-subnet-b", {
    cidrBlock: "10.0.1.0/24",
    vpc: vpc,
    igw: igw,
    routeTable:routeTable,
    az: "ap-northeast-1b",
  }),
  vpcx.generateSubnet("eks-subnet-c", {
    cidrBlock: "10.0.2.0/24",
    vpc: vpc,
    igw: igw,
    routeTable:routeTable,
    az: "ap-northeast-1c"
  }),
  vpcx.generateSubnet("eks-subnet-d", {
    cidrBlock: "10.0.3.0/24",
    vpc: vpc,
    igw: igw,
    routeTable:routeTable,
    az: "ap-northeast-1d"
  })
];

3.Security Group

Security Groupは、共通化する要素がなかったため、Terraformのコードを参考にそのまま移植しました。

src/3.sg.ts
import * as aws from "@pulumi/aws";
import { vpc } from "./2.vpc";

export const clusterMasterSG = new aws.ec2.SecurityGroup("cluster-master", {
  name: "cluster-master",
  description: "EKS cluster master security group",

  vpcId: vpc.id,

  ingress: [
    {
      fromPort: 443,
      toPort: 443,
      protocol: "tcp",
      cidrBlocks: ["0.0.0.0/0"]
    }
  ],

  egress: [
    {
      fromPort: 0,
      toPort: 0,
      protocol: "-1",
      cidrBlocks: ["0.0.0.0/0"]
    }
  ]
});

export const clusterWorkerSG = new aws.ec2.SecurityGroup("cluster-worker", {
  name: "cluster-worker",
  description: "EKS cluster worker security group",

  vpcId: vpc.id,

  ingress: [
    {
      description: "Allow cluster master to access cluster nodes",
      fromPort: 1025,
      toPort: 65535,
      protocol: "tcp",
      securityGroups: [clusterMasterSG.id]
    },

    {
      description: "Allow cluster master to access cluster nodes",
      fromPort: 1025,
      toPort: 65535,
      protocol: "udp",
      securityGroups: [clusterMasterSG.id]
    },

    {
      description: "Allow inter pods communication",
      fromPort: 0,
      toPort: 0,
      protocol: "-1",
      self: true
    }
  ],

  egress: [{
      fromPort: 0,
      toPort: 0,
      protocol: "-1",
      cidrBlocks: ["0.0.0.0/0"]
  }]

});

4.EKS

EKSは以下の2点を共通関数に切り出しました。

  1. kubeconfigの設定ファイルの生成
  2. auth-awsのConfigMapの生成
src/func/k8sx.ts
import * as aws from "@pulumi/aws";
import * as k8s from "@pulumi/kubernetes";
import * as pulumi from "@pulumi/pulumi";

export function kubeconfig(eksCluster: aws.eks.Cluster) {
  return pulumi
    .all([
      eksCluster.endpoint,
      eksCluster.certificateAuthority,
      eksCluster.name
    ])
    .apply(([endpoint, certificateAuthority, name]) => {
      return `
  apiVersion: v1
  clusters:
  - cluster:
      server: ${endpoint}
      certificate-authority-data: ${certificateAuthority.data}
    name: kubernetes
  contexts:
  - context:
      cluster: kubernetes
      user: aws
    name: aws
  current-context: aws
  kind: Config
  preferences: {}
  users:
  - name: aws
    user:
      exec:
        apiVersion: client.authentication.k8s.io/v1alpha1
        command: aws-iam-authenticator
        args:
          - "token"
          - "-i"
          - "${name}"
  `;
    });
}

export function awsAuthConfigMap(
  eksWorkerRole: aws.iam.Role,
  provider: k8s.Provider
) {
  return new k8s.core.v1.ConfigMap(
    "aws-auth",
    {
      metadata: {
        name: "aws-auth",
        namespace: "kube-system"
      },
      data: {
        mapRoles: eksWorkerRole.arn.apply(arn => {
          return `- rolearn: ${arn}
  username: system:node:{{EC2PrivateDNSName}}
  groups:
   - system:bootstrappers
   - system:nodes
`;
        })
      }
    },
    {
      provider: provider
    }
  );
}

EKSのクラスターの作成に加えて、Kubernetesのプロバイダーを利用して、aws-authの設定もここで行う構成にしました。

src/4.eks.ts
import * as k8s from "@pulumi/kubernetes";
import * as aws from "@pulumi/aws";

import * as k8sx from "./func/k8sx";

import * as iam from "./1.iam";
import {subnets} from './2.vpc';
import {clusterMasterSG} from './3.sg';

export const eksCluster = new aws.eks.Cluster("eks-cluster", {
  roleArn: iam.eksMasterRole.role.arn,
  vpcConfig: {
      securityGroupIds : [clusterMasterSG.id],
      subnetIds: subnets.map(subnet=>subnet.id),
  },
},
{
  dependsOn:[iam.eksMasterRole.amazonEKSClusterPolicy,iam.eksMasterRole.amazonEKSServicePolicy]
}
);

export const kubeconfig = k8sx.kubeconfig(eksCluster);

export const provider = new k8s.Provider("k8s",{
  kubeconfig:kubeconfig
})

export const configMap = k8sx.awsAuthConfigMap(iam.eksWorkerRole.role,provider);

5. Auto Scaling Group

Auto Scaling Group関連では、Worker Nodeに設定する UserData の生成 と Worker Node 用のAMIイメージを取得する処理を共通関数として切り出しました。

src/func/k8sx.ts
export function eksUserDataBase64(eksCluster: aws.eks.Cluster) {
  return pulumi
    .all([
      eksCluster.endpoint,
      eksCluster.certificateAuthority,
      eksCluster.name
    ])
    .apply(([endpoint, certificateAuthority, name]) => {
      const userdata = `#!/bin/bash
set -o xtrace
/etc/eks/bootstrap.sh --apiserver-endpoint "${endpoint}" --b64-cluster-ca "${certificateAuthority.data}" "${name}"
`;
      var buffer1 = Buffer.from(userdata, "ascii");
      var base64 = buffer1.toString("base64");
      return base64;
    });
}

export function getEksAmi(eksCluster: aws.eks.Cluster) {
  return eksCluster.version.apply(version => {
    return aws.getAmi({
      mostRecent: true,
      owners: ["602401143452"],
      filters: [
        {
          name: "name",
          values: [`amazon-eks-node-${version}-*`]
        }
      ]
    });
  });
}

launchConfiguration と Auto Scaling Group の設定をTerraformを参考にコーディングしました。

src/5.asg.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

import * as k8sx from "./func/k8sx";

import * as iam from "./1.iam";
import * as vpc from "./2.vpc";
import * as sg from "./3.sg";
import * as eks from './4.eks';

const baseName = `sample-pulumi-${pulumi.getStack()}`;

export const userDataBase64 = k8sx.eksUserDataBase64(eks.eksCluster);
export const eksNode = k8sx.getEksAmi(eks.eksCluster);

export const launchConfiguration = new aws.ec2.LaunchConfiguration("eks-lc", {
  associatePublicIpAddress: true,
  iamInstanceProfile: iam.eksWorkerRole.instanceProfile.id,
  imageId: eksNode.imageId,
  instanceType: "t3.medium",
  namePrefix: "eks-node",
  enableMonitoring: false,

  rootBlockDevice: {
    volumeType: "gp2",
    volumeSize: 50
  },

  securityGroups: [sg.clusterWorkerSG.id],
  userDataBase64: userDataBase64
});

export const eksAsg = new aws.autoscaling.Group("eks-asg", {
  name: "EKS cluster nodes",
  desiredCapacity: 3,
  launchConfiguration : launchConfiguration.id,
  maxSize: 8,
  minSize: 2,
  vpcZoneIdentifiers: vpc.subnets.map(subnet => subnet.id),
  tags: [
    {
      key: "Name",
      value: `${baseName}-nodes`,
      propagateAtLaunch: true
    },
    {
      key: eks.eksCluster.name.apply(name=>`kubernetes.io/cluster/${name}`) ,
      value: "owned",
      propagateAtLaunch: true
    },

    {
      key: "Project",
      value: `${baseName}-project`,
      propagateAtLaunch: true
    },

    {
      key: "Terraform",
      value: "true",
      propagateAtLaunch: true
    },
  ]
});

6. Kubernetes の疎通用のサンプル

疎通用のサンプルとして、Namespaceを作り、NginxをDeploymentに登録し、Nginxを公開するServiceをコーディングしました。

src/6.k8s-nginx.ts
import * as k8s from "@pulumi/kubernetes";

import * as eks from "./4.eks";

const provider = eks.provider;

const appName = "hello-world";
const namespaceName = new k8s.core.v1.Namespace(
  appName,
  {
    metadata: {
      name: appName
    }
  },
  {
    provider: provider
  }
);

const nginxLabels = { app: "nginx" };
export const nginxDeployment = new k8s.apps.v1.Deployment(
  "nginx-deployment",
  {
    metadata: {
      name: appName,
      namespace: namespaceName.metadata.name
    },
    spec: {
      selector: { matchLabels: nginxLabels },
      replicas: 2,
      template: {
        metadata: { labels: nginxLabels },
        spec: {
          containers: [
            {
              name: "nginx",
              image: "nginx:1.7.9",
              ports: [{ containerPort: 80 }]
            }
          ]
        }
      }
    }
  },
  {
    provider: provider
  }
);

// Expose proxy to the public Internet.
const frontend = new k8s.core.v1.Service(appName, {
  metadata: { 
    labels: nginxDeployment.spec.template.metadata.labels,
    namespace: namespaceName.metadata.name
  },
  spec: {
      type: "LoadBalancer",
      ports: [{ port: 80, targetPort: 80, protocol: "TCP" }],
      selector: nginxLabels,
  },
}, { provider: provider });

// Export the frontend IP.
export const frontendIp = frontend.status.loadBalancer.ingress[0].ip;

所感

TypeScriptでコーディングできるので、コード補完されることに加えて、gulp.watchとも連携させることができて、Hot Reloading しながら、Infra as Code の構成を確認していけるのは楽でした。

”画面を開発するようにクラウドを開発できる!" と、期待していたのですが、そもそも EKSのクラスターの作成には10分近く時間がかかるので、Hot Reloadingは、pulumi previewまでにしました。クラウドに反映させる場合には、時間のかかるコンポーネントを外す必要がありますね。

HotReloadingに利用したgulpfile.js
var gulp = require('gulp');
const exec = require('child_process').exec;

gulp.task('watch', function () {
  gulp.watch('./*.ts', gulp.task('preview'));
});

gulp.task('preview', cb => {
  var spawn = require('child_process').spawn,
    preview = spawn('pulumi', ["preview"], {
      stdio: 'inherit',
      env: {
        PULUMI_CONFIG_PASSPHRASE: ""
      }
    });
  preview.on('exit', function (code) {
    cb()
  });
});

gulp.task('default', gulp.task('watch'));
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JS 配列を更に小さい配列に分割する array_chunk

UdemyのThe Coding Interview Bootcamp: Algorithms + Data Structuresで勉強したことを呟きます:boy_tone1:

Chapter6【array_chunk】
配列を小さい配列の集まり、塊(チャンク)に分割する。サイズは第2引数に指定する。余りが出たら次の配列を作成する。

(例)

chunk([1, 2, 3, 4], 2) --> [[ 1, 2], [3, 4]]
chunk([1, 2, 3, 4, 5], 2) --> [[ 1, 2], [3, 4], [5]]
chunk([1, 2, 3, 4, 5, 6, 7, 8], 3) --> [[ 1, 2, 3], [4, 5, 6], [7, 8]]
chunk([1, 2, 3, 4, 5], 4) --> [[ 1, 2, 3, 4], [5]]
chunk([1, 2, 3, 4, 5], 10) --> [[ 1, 2, 3, 4, 5]]

1. 最後の配列とサイズを比較:rolling_eyes:

function chunk(array, size) {
  const chunked = [];

  for (let element of array) {
    const last = chunked[chunked.length - 1];

    if (!last || last.length === size) {
      chunked.push([element]);
    } else {
      last.push(element);
    }
  }

  return chunked;
}
  1. 個別の配列が入る大枠の配列を作成(chunked)、
  2. 配列の要素をfor...ofでループし、それぞれの数字のvalueをelementという変数に格納
  3. 最後にチャンクされた配列(今現在変数を格納されている途中のチャンク)をlastという変数に定義する。
  4. もしlastがない(チャングされた配列がない)、またはチャンクされた配列の要素サイズが、指定されたサイズと等しいなら新しい配列チャンクを作成
  5. それ以外は現在の配列チャンクに追加する

2. sliceを使う:grinning:

function chunk(array, size) {
    const chunked = [];
    let index = 0;

    while (index < array.length) {
        chunked.push(array.slice(index, index + size));
        index += size;
    }

    return chunked;
}
  1. 個別の配列が入る大枠の配列を作成(chunked)
  2. 0から始まるindexを作成
  3. もしindexが最初に渡された配列のサイズよりも小さければ新規にchunkの中に配列を追加
  4. indexが元の配列のサイズを超えたらループを終了しreturnする

参考リンク

Array.prototype.slice()
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/slice

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

JS 配列を更に小さい配列に分割する

UdemyのThe Coding Interview Bootcamp: Algorithms + Data Structuresで勉強したことを呟きます:boy_tone1:

Chapter6【Array Chunking】
配列を小さい配列の集まり、塊(チャンク)に分割する。サイズは第2引数に指定する。余りが出たら次の配列を作成する。

(例)

chunk([1, 2, 3, 4], 2) --> [[ 1, 2], [3, 4]]
chunk([1, 2, 3, 4, 5], 2) --> [[ 1, 2], [3, 4], [5]]
chunk([1, 2, 3, 4, 5, 6, 7, 8], 3) --> [[ 1, 2, 3], [4, 5, 6], [7, 8]]
chunk([1, 2, 3, 4, 5], 4) --> [[ 1, 2, 3, 4], [5]]
chunk([1, 2, 3, 4, 5], 10) --> [[ 1, 2, 3, 4, 5]]

1. 最後の配列とサイズを比較:rolling_eyes:

function chunk(array, size) {
    const chunked = [];

    for (let element of array) {
        const last = chunked[chunked.length - 1];

        if (!last || last.length === size) {
            chunked.push([element]);
        } else {
            last.push(element);
        }
    }

    return chunked;
}
  1. 個別の配列が入る大枠の配列を作成(chunked)、
  2. 配列の要素をfor...ofでループし、それぞれの数字のvalueをelementという変数に格納
  3. 最後にチャンクされた配列(今現在変数を格納されている途中のチャンク)をlastという変数に定義する。
  4. もしlastがない(チャングされた配列がない)、またはチャンクされた配列の要素サイズが、指定されたサイズと等しいなら新しい配列チャンクを作成
  5. それ以外は現在の配列チャンクに追加する

2. sliceを使う:grinning:

function chunk(array, size) {
    const chunked = [];
    let index = 0;

    while (index < array.length) {
        chunked.push(array.slice(index, index + size));
        index += size;
    }

    return chunked;
}
  1. 個別の配列が入る大枠の配列を作成(chunked)
  2. 0から始まるindexを作成
  3. もしindexが最初に渡された配列のサイズよりも小さければ新規にchunkの中に配列を追加
  4. indexが元の配列のサイズを超えたらループを終了しreturnする

参考リンク

Array.prototype.slice()
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/slice

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

配列を更に小さい配列に分割する

UdemyのThe Coding Interview Bootcamp: Algorithms + Data Structuresで勉強したことを呟きます:boy_tone1:

Chapter6【Array Chunking】
配列を小さい配列の集まり、塊(チャンク)に分割する。サイズは第2引数に指定する。余りが出たら次の配列を作成する。

(例)

chunk([1, 2, 3, 4], 2) --> [[ 1, 2], [3, 4]]
chunk([1, 2, 3, 4, 5], 2) --> [[ 1, 2], [3, 4], [5]]
chunk([1, 2, 3, 4, 5, 6, 7, 8], 3) --> [[ 1, 2, 3], [4, 5, 6], [7, 8]]
chunk([1, 2, 3, 4, 5], 4) --> [[ 1, 2, 3, 4], [5]]
chunk([1, 2, 3, 4, 5], 10) --> [[ 1, 2, 3, 4, 5]]

1. 最後の配列とサイズを比較:rolling_eyes:

function chunk(array, size) {
    const chunked = [];

    for (let element of array) {
        const last = chunked[chunked.length - 1];

        if (!last || last.length === size) {
            chunked.push([element]);
        } else {
            last.push(element);
        }
    }

    return chunked;
}
  1. 個別の配列が入る大枠の配列を作成(chunked)、
  2. 配列の要素をfor...ofでループし、それぞれの数字のvalueをelementという変数に格納
  3. 最後にチャンクされた配列(今現在変数を格納されている途中のチャンク)をlastという変数に定義する。
  4. もしlastがない(チャングされた配列がない)、またはチャンクされた配列の要素サイズが、指定されたサイズと等しいなら新しい配列チャンクを作成
  5. それ以外は現在の配列チャンクに追加する

2. sliceを使う:grinning:

function chunk(array, size) {
    const chunked = [];
    let index = 0;

    while (index < array.length) {
        chunked.push(array.slice(index, index + size));
        index += size;
    }

    return chunked;
}
  1. 個別の配列が入る大枠の配列を作成(chunked)
  2. 0から始まるindexを作成
  3. もしindexが最初に渡された配列のサイズよりも小さければ新規にchunkの中に配列を追加
  4. indexが元の配列のサイズを超えたらループを終了しreturnする

参考リンク

Array.prototype.slice()
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/slice

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

配列を分割する

UdemyのThe Coding Interview Bootcamp: Algorithms + Data Structuresで勉強したことを呟きます:boy_tone1:

Chapter6【Array Chunking】
配列を小さい配列の集まり、塊(チャンク)に分割する。サイズは第2引数に指定する。余りが出たら次の配列を作成する。

(例)

chunk([1, 2, 3, 4], 2) --> [[ 1, 2], [3, 4]]
chunk([1, 2, 3, 4, 5], 2) --> [[ 1, 2], [3, 4], [5]]
chunk([1, 2, 3, 4, 5, 6, 7, 8], 3) --> [[ 1, 2, 3], [4, 5, 6], [7, 8]]
chunk([1, 2, 3, 4, 5], 4) --> [[ 1, 2, 3, 4], [5]]
chunk([1, 2, 3, 4, 5], 10) --> [[ 1, 2, 3, 4, 5]]

1. 最後の配列とサイズを比較:rolling_eyes:

function chunk(array, size) {
    const chunked = [];

    for (let element of array) {
        const last = chunked[chunked.length - 1];

        if (!last || last.length === size) {
            chunked.push([element]);
        } else {
            last.push(element);
        }
    }

    return chunked;
}
  1. 個別の配列が入る大枠の配列を作成(chunked)、
  2. 配列の要素をfor...ofでループし、それぞれの数字のvalueをelementという変数に格納
  3. 最後にチャンクされた配列(今現在変数を格納されている途中のチャンク)をlastという変数に定義する。
  4. もしlastがない(チャングされた配列がない)、またはチャンクされた配列の要素サイズが、指定されたサイズと等しいなら新しい配列チャンクを作成
  5. それ以外は現在の配列チャンクに追加する

2. sliceを使う:grinning:

function chunk(array, size) {
    const chunked = [];
    let index = 0;

    while (index < array.length) {
        chunked.push(array.slice(index, index + size));
        index += size;
    }

    return chunked;
}
  1. 個別の配列が入る大枠の配列を作成(chunked)
  2. 0から始まるindexを作成
  3. もしindexが最初に渡された配列のサイズよりも小さければ新規にchunkの中に配列を追加
  4. indexが元の配列のサイズを超えたらループを終了しreturnする

参考リンク

Array.prototype.slice()
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/slice

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

TypescriptとReact HooksでReduxはもうしんどくない

?背景

Reduxはしんどい、だるい、でかい、というイメージが定着して久しいですね ?

僕も3年ほど前にどっぷり触ったときは「こいつぁなかなか」という感想でした。
しかしながら状態管理ライブラリやらFlux思想やらの流れとしてReduxが不可避の存在だったために、おつらい経験をされた方も多かったのかなとお察しします。

時代は巡り2019年末、令和元年のご時世ではすっかりTypescriptによる型安全、正式提供されたReact Hooksによる脱Class component・HOCフリーな省エネ設計などが定着してきており、この両者を前提とした構築がもはやスタンダードとなってきています。

諸兄の人柱的知見も相まって最近は敬遠されがちなReduxパイセンですが、この度久方ぶりにがっつりと向き合ってみると、上述した両者の恩恵を受けてなんだか垢抜けた感じになっていました。知ってましたか? ?‍?

といった趣向の記事です。

?‍♀️ この記事でやらないこと

状態管理ライブラリ比較

  • MobXのほうが〜、Context APIとくらべて〜、とかのあたり
    • ぶっちゃけ一長一短なので導入するべきケースとかを比較しだすと記事長が30mくらいになるので
    • ジハードがしたいわけではない

Redux不要論へのアンサー

  • すみませんAdvent Calendar一日目の方の内容への当てこすり記事ではないんです ?タイミング悪くて申し訳ない…
  • Reduxサイコ〜〜みんなRedux使うともれなく幸せになれます ?‍♀️ といった類の記事ではないです

その他推奨ライブラリの細かいHow to

  • immerやらreselectやらの絶対使ったほうがいいライブラリってのはあるんですが、詳しい使い方とかはここでは書きません ?‍♂️
  • 非同期処理に関しては個人的にredux-observableがダントツいいんじゃないかな〜とは思っていますが、RxJSをちょっとは知っとかないとダメだったり、Type定義あたりでちょこちょこストレスがあったり、という状況なので今回は言及しません ?
    • 正直middlewareでうまいことやれてしまう説もあります。

? サンプル

https://gitlab.com/Ky7ie/redux_a_g0_g0

突貫ですみませんがサンプルリポジトリを用意しました。
状態管理部分の実装サンプルとしてご参照していただければと思っています。
(その他のとことかはいろいろ手を抜いています)

? つらくないポイント解説

ActionとReducerをTypeScriptでさらっと面倒みてあげられる

結局のところ、素でReduxつかうとしんどいところって
「記述が分断されていて定義とかを追うのが大変… ?」
とか
「他人が書いたActionやらReducerをぐるっと追って理解しないと実装できないのキツ〜〜〜 ?」
という不透明性、見通しの悪さによるところが多かったなあという印象なんですがいかがでしょう。

Typescript(およびTS連携できるIDE環境)の恩恵によって、そのあたりの「どこみたらいいか全然わからんし頭に全く入ってこないぞ ?‍♀️」がほぼ無くなりました。

Action

自前でちゃんとType定義してもいいですが、定義支援ライブラリの typescript-fsa を使うと記述がむちゃ簡素で気持ちいいです。

store/todo/actions/index.ts
import { actionCreatorFactory } from 'typescript-fsa';

const actionCreator = actionCreatorFactory('TODO');

export const Add = actionCreator<{ title: string }>('ADD');
export const ChangeStatus = actionCreator<{ index: number }>('CHANGE_STATUS');
export const Delete = actionCreator<{ index: number }>('DELETE');

こう記述すると fsa = Flux Standard Actionに則ったActionをType定義つきで生成してくれます ?

Reducer

ReducerもType定義でちゃんと縛っておくと、後から処理を記述するときなどにヒジョーに快適です。

store/todo/reducer/index.ts
import { Reducer } from 'redux';
import { isType } from 'typescript-fsa';
import { produce } from 'immer';

import { Todo } from '..';
import { Add, ChangeStatus, Delete } from '..';

const initialState: Todo[] = [];

export const reducer: Reducer<Todo[]> = (state = initialState, action) => {
  if (isType(action, Add)) {
    return produce(state, draft => {
      draft.push({ title: action.payload.title, status: 'not yet' });
    });
  }

// 中略...

  return state;
};

export default reducer;


Dec-01-2019 15-21-28_c.gif

?

State

ちょっとしたTipsですが、State全体のType定義はReturnTypeを使うことでサボれます。

store/index.ts
import { combineReducers, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';

import todo from './todo';

const reducers = combineReducers({ todo });

export const Store = createStore(reducers, composeWithDevTools());

export type State = ReturnType<typeof Store.getState>;

React Hooksを使うとconnect()もmapStateToPropsもいらなくなる

個人的感動ポイントがここです ?

Reduxを勉強し始めたときも、書き始めてからも、

  • connect()()
  • mapStateToProps()
  • mapDispatchToProps()

このあたりの「おなじみ呪文シリーズ ?‍♀️」がどうにも気持ち悪く

(ヤダな〜〜 ?)

と苦々しく思っていたのですが、React Hooksの恩恵によりこれらがスッキリとした基準に置き換えられ、すべてをFunctional Componentとして記述できるようになりました ?✨

components/organisms/todo/index.tsx
import React from 'react';
import { useDispatch, useSelector } from 'react-redux'; // ココ!
import { Todo as TodoType, Add, GetAllTodos } from 'store/todo';

export const Todo: React.FC = () => {
  // 略... //

  // dispatcherを用意
  const dispatch = useDispatch();

  // これだけでstateを参照できる
  const todos = useSelector(GetAllTodos);

  const onSubmit = methods.handleSubmit(({ title }) => {
    // これだけでactionをdispatchできる
    dispatch(Add({ title }));

    // 略... //

  });

  return (
    <Container>

      // 略... //

      {todos.map((todo, index) => (
        <TodoCard key={index} index={index} {...todo} />
      ))}
    </Container>
  );
};

store/todo/selectors/index.ts
import { createSelector } from 'reselect';
import { State } from 'store';

import { Todo } from '..';

export const GetAllTodos = createSelector(
  (state: State) => state.todo,
  (todos: Todo[]) => todos
);

Selectorによるstateのメモ化がもたらしてくれるパフォーマンスチューニング的意義に関しての解説アレコレ〜〜〜〜は今回は省きます ?

が、単純にコードの見通しだけ考えても非常に簡潔化でき、読みやすくなったのではないでしょうか ?

コラム : Re-ducksパターン ?

「ActionとかReducerとか記述散らばるのがしんどいぜ」というReduxあるあるがありましたが、そのあたりのディレクトリ構成に関しても研究が進んでおりRe-ducksパターンというものが個人的には一番しっくりきました ???

アレコレと合理的な理由付けと経緯があるのですが、よくまとめていただいている記事がありましたので... ?

そちらを読んでいただいた上でサンプルリポジトリの構成をご参照いただけるとスッキリ理解していただけるかと思います。

? Reduxはまだまだ有力な選択肢

フロントエンド技術の発展とトレンドの移り変わりがReduxを押し流してしまったように勝手に感じていたのですが、見つめ直してみると

「Redux全然イケてるじゃん ?」

と惚れ直す結果となりました。

Middlewareや強力なDevtoolなど、単純な状態管理にとどまらない部分も依然として魅力的なライブラリです。

これから改めて学ぶのも全然アリだと思います!

参考

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

Vue.jsで動的にコンポーネントを生成・削除・マウントする方法

はじめに

普段Vue.jsを使って開発をしているのですが、ある日、開発をしていたらコンポーネントを動的にマウントしたいという希望が出てきました。

この記事では、Vueのコンポーネントを動的にマウント、それに伴い動的にマウントしたコンポーネントを削除したり、propsを設定したりする方法について説明します。

どんな時に使うか

例えば、ボタンを押したらテキストエリアが出現するコードについて考えてみます。

これぐらいであればjQueryでできるかもしれませんが、テキストボックスにcssで装飾などをつけている場合には、コンポーネントにして、それをappendしたいと思います。Vueを使えばイベントの定義なども非常に簡単になるので一石二鳥です。

v-ifやv-showなどでもできますが、コンポーネントを最初どこに追加するかわからない場合などには動的にマウントする必要が出てくるでしょう。

ただ単にコンポーネントを表示するだけであれば以下のようになります。テキストエリアの横には文字数のカウントをつけています。

スクリーンショット 2019-11-30 22.33.40.png

vueファイルはこんな感じ。

<template>
  <div>
    <count-text></count-text>
    <button>click me!</button>
  </div>
</template>

<script>
import TextBox from './TextBox.vue';

export default {
  components: {
    'count-text': TextBox
  }
}
</script>
TextBox.vue
<template>
  <div class="uk-margin-top uk-margin-left">
    <textarea rows=5 v-model="text" @keydown="countTextLength" @keyup="countTextLength"></textarea>
    <span>{{ length }}文字</span>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      length: 0,
      text: ''
    }
  },
  methods: {
    countTextLength: function () {
      this.length = this.text.length;
    }
  }
}
</script>

コンポーネントを動的にマウントする

ポイントは、appendTextメソッドの中です。
コンポーネントのインスタンスを作成して、jQueryでappendしています。

<template>
  <div>
    <div class="text-place"></div>
    <button @click="appendText">click me!</button>
  </div>
</template>

<script>
import Vue from 'vue/dist/vue.esm.js';
import TextBox from './TextBox.vue';

export default {
  methods: {
    appendText: function () {
      var ComponentClass = Vue.extend(TextBox);
      var instance = new ComponentClass();
      instance.$mount();
      $('.text-place').append(instance.$el);
    }
  }
}
</script>

画面は以下のような感じになります。

xeot6-o3tb8.gif

propsを設定する

動的にマウントするコンポーネントにpropsを設定したい時にはどうすれば良いのでしょうか。

テキストにtitleを設定してみます。

TextBox.vue
<template>
  <div class="uk-margin-top uk-margin-left">
    <span>{{ title }}</span>
    <textarea rows=5 v-model="text" @keydown="countTextLength" @keyup="countTextLength"></textarea>
    <span>{{ length }}文字</span>
  </div>
</template>

<script>
export default {
  props: {
    title: String //追加
  },
  data: function () {
    return {
      length: 0,
      text: ''
    }
  },
  methods: {
    countTextLength: function () {
      this.length = this.text.length;
    }
  }
}
</script>

propsを設定するには、以下のようにします。

<template>
  <div>
    <div class="text-place"></div>
    <button @click="appendText">click me!</button>
  </div>
</template>

<script>
import Vue from 'vue/dist/vue.esm.js';
import TextBox from './TextBox.vue';

export default {
  methods: {
    appendText: function () {
      var ComponentClass = Vue.extend(TextBox)
      var instance = new ComponentClass({
        // 追加
        propsData: {
          title: 'タイトル'
        }
      });
      instance.$mount();
      $('.text-place').append(instance.$el);
    }
  }
}
</script>

スクリーンショット 2019-12-01 12.28.17.png

イベントを設定する

emitなどで親にイベントを通知したい時などは以下のようにします。

<template>
  <div class="uk-margin-top uk-margin-left">
    <span>{{ title }}</span>
    <textarea rows=5 v-model="text" @keydown="countTextLength" @keyup="countTextLength" @change="changeText"></textarea>
    <span>{{ length }}文字</span>
  </div>
</template>

<script>
export default {
  props: {
    title: String
  },
  data: function () {
    return {
      length: 0,
      text: ''
    }
  },
  methods: {
    countTextLength: function () {
      this.length = this.text.length;
    },
    changeText: function () {
      this.$emit('changed'); //追加
    }
  }
}
</script>

親のコンポーネントは以下の通り

<template>
  <div>
    <div class="text-place"></div>
    <button @click="appendText">click me!</button>
  </div>
</template>

<script>
import Vue from 'vue/dist/vue.esm.js';
import TextBox from './TextBox.vue';

export default {
  methods: {
    appendText: function () {
      var ComponentClass = Vue.extend(TextBox)
      var instance = new ComponentClass({
        propsData: {
          title: 'タイトル'
        }
      });
      // emitで受け取るイベントを定義 functionの部分はmethodsで定義もできます。
      instance.$on('change', function () {
        console.log('textchanged');
      });
      instance.$mount();
      $('.text-place').append(instance.$el);
    }
  }
}
</script>

instance.$on('イベント名', 処理)という感じで定義できます。

コンポーネントを削除するには

自分自身を削除するには$destroyを使います。

<template>
  <div class="uk-margin-top uk-margin-left">
    <span>{{ title }}</span>
    <textarea rows=5 v-model="text" @keydown="countTextLength" @keyup="countTextLength" @change="changeText"></textarea>
    <span>{{ length }}文字</span>
    <button @click="deleteSelf">destroy!</button>
  </div>
</template>

<script>
export default {
  props: {
    title: String
  },
  data: function () {
    return {
      length: 0,
      text: ''
    }
  },
  methods: {
    countTextLength: function () {
      this.length = this.text.length;
    },
    changeText: function () {
      this.$emit('changed');
    },
    deleteSelf: function () {
      this.$destroy();
      this.$el.parentNode.removeChild(this.$el);
    }
  }
}
</script>
      this.$destroy();
      this.$el.parentNode.removeChild(this.$el);

でコンポーネントを削除します。
thisの部分をinstanceに変更すると、親からも削除できます。

      instance.$destroy();
      instance.$el.parentNode.removeChild(instance.$el);

終わりに

Vue.jsは非常に便利だと感じました。
この場を借りて、この方法を教えてくれた参考記事の偉大な先人たちへ深い感謝を送りたいと思います。
非常に助かりました。

参考記事

https://css-tricks.com/creating-vue-js-component-instances-programmatically/
https://stackoverflow.com/questions/40445125/how-can-component-delete-itself-in-vue-2-0

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