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

[Angular] テキストエリアとボタンを使ったコンポーネントのテストを書く

環境

  • Angular CLI: 7.3.1

たぶん他のバージョンでも動く

準備

事前準備

テキストエリアやボタンを使うのにFormsModule必要です。

app.modules.ts
import { FormsModule } from '@angular/forms';
// ...
  imports: [
    FormsModule,
// ...
app.component.spec.ts
  beforeEach(async(() => {
    TestBed.configureTestingModule({
// ...
      imports: [
        FormsModule
// ...

HTMLとTypeScriptの準備

app.component.html
<div id="message">{{message}}</div>
<textarea [(ngModel)]="input"></textarea>
<button (click)="postMessage(input)">書く</button>
app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  message = '';
  input = '';

  postMessage(message: string) {
    this.message = message;
    this.input = '';
  }

}

あると便利なもの

app.component.spec.ts
  beforeEach(async(() => {
    TestBed.configureTestingModule({
// ...
      providers: [
        { provide: ComponentFixtureAutoDetect, useValue: true }
// ...

CompoentFixtureAutoDetectはある程度コンポーネント変更検知を自動検知してくれる。
何度もdetectChanges()書かなくて済む
まぁ、DOMイベントは自動検知してくれないけど(以下のサンプルでは必須)。

本題

inputイベントが発行されたことをテストする

app.compoent.spec.ts
it('should input to textarea', () => {
  const fixture = TestBed.createComponent(AppComponent);
  const app = fixture.componentInstance;
  const htmlElement: HTMLElement = fixture.debugElement.nativeElement;
  const textarea = htmlElement.querySelector('textarea');

  textarea.value = 'なんか書いた';               // 注目
  textarea.dispatchEvent(new Event('input')); // 注目
  fixture.detectChanges();                    // 注目

  expect(app.message).toBe('なんか書いた');
});

ちなみにquerySelectorに渡す文字列はCSSのセレクターの指定の仕方と同じ

ボタンをクリックをテストする

app.component.spec.ts
it('should post message by clicking button', () => {
  const fixture = TestBed.createComponent(AppComponent);
  const htmlElement: HTMLElement = fixture.debugElement.nativeElement;
  const textarea = htmlElement.querySelector('textarea');
  const button = htmlElement.querySelector('button');
  const message = htmlElement.querySelector('div#message');

  textarea.value = 'なんか書いた';
  textarea.dispatchEvent(new Event('input'));
  button.dispatchEvent(new Event('click')); // 注目
  fixture.detectChanges();

  expect(message.htmlContent).toBe('なんか書いた');
});

エンターキーで投稿できるようにする

keydown.enterのコールバックを指定する

app.component.html
<div id="message">{{message}}</div>
<textarea [(ngModel)]="input" (keydown.enter)="postMessage(input)"></textarea>
<button (click)="postMessage(input)">書く</button>

KeyboardEventを発行してテストする

app.component.spec.ts
it('should post message by pressing enter key', () => {
  const fixture = TestBed.createComponent(AppComponent);
  const htmlElement: HTMLElement = fixture.debugElement.nativeElement;
  const textarea = htmlElement.querySelector('textarea');
  const message = htmlElement.querySelector('div#message');

  textarea.value = 'なんか書いた';
  textarea.dispatchEvent(new Event('input'));
  textarea.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'})); // 注目
  fixture.detectChanges();

  expect(message.htmlContent).toBe('なんか書いた');
});

参考記事

間違いなどありましたら気兼ねなくご指摘ください

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

IEX APIで暗号通貨のレート情報を一覧表示

IEX APIに暗号通貨の情報を取得するものがあったので、それを使用してレート情報を一覧形式で表示します(10秒ごとに自動更新)。

実行イメージ

下記画像のような表示になります。
image.png

実際に動いている様子は下記記事で見れます。
【プログラミング】IEX APIで暗号通貨のレート情報を一覧表示【JavaScript】

コード

そのままHTMLファイルとして保存すれば動きます。

crypto_rate_list.html
<div id="crypt_table"> </div>

<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script language="javascript">// <![CDATA[
window.onload = function () {
    InitProc();
    MainProc();
    AutoUpdate();
}

function InitProc(){
    //グローバル変数の初期設定
    currency_list = ['BTCUSDT', 'EOSUSDT', 'ETHUSDT', 'BNBUSDT', 'ONTUSDT', 'BCCUSDT', 
                     'ADAUSDT', 'XRPUSDT', 'TUSDUSDT', 'TRXUSDT', 'LTCUSDT', 'ETCUSDT', 
                     'IOTAUSDT', 'ICXUSDT', 'NEOUSDT', 'VENUSDT', 'XLMUSDT', 'QTUMUSDT'];
    table_data = [];
    update_flg = 0;
    edit_roop_cnt = 1;
}

function AutoUpdate(){
    //10秒ごとに更新
    setInterval(MainProc,10000);
}

function MainProc(){
    var table_header = ['No','Symbol', 'Name', 'latestPrice', 'latestVolume', 'change', 'changePercent'];

    //Edit Header
    table_data[0] = [];
    for(var i = 0; i < table_header.length; i++) {
        table_data[0][i] = table_header[i];
    }

    //Get & Edit Info
    for(var j = 0; j < currency_list.length; j++) {
        in_currency = currency_list[j];
        getCryptoInfo(in_currency, editInfo);
    }
}

function getCryptoInfo(currency, callback){
    $.ajax({
        url : 'https://api.iextrading.com/1.0/stock/' + currency + '/quote',
        type : 'GET',       
        async : true,        
        cashe : false,     
        dataType : 'json',  
        contentType : 'application/json' 
    }).done(function(result){
        callback(result);
    }).fail(function(result){
        alert('Failed to load the information');
        console.log(result)
    });  
}

function editInfo(result){
    var edit_data = [];
    var edit_change_per = result.changePercent.toFixed(4) + '%';
    var currency_num;

    for(var i = 0; i < currency_list.length; i++) {
        if(result.symbol == currency_list[i]){
            currency_num = i + 1;
            edit_data = [currency_num, result.symbol, result.companyName, result.latestPrice, result.latestVolume, result.change, edit_change_per];
            table_data[currency_num] = [];
            for(var j = 0; j < edit_data.length; j++) {
                table_data[currency_num][j] = edit_data[j];
            }
        }
    }

    if(edit_roop_cnt==currency_list.length){
        edit_roop_cnt = 1;
        if(update_flg==0){
            makeTable(table_data,"crypt_table");
            update_flg = 1;
        }else{
            updateTable(table_data,"crypt_table");
        }
    }else{
        edit_roop_cnt++
    }
}

//【Javascript】表(table)の動的作成:https://algorithm.joho.info/programming/javascript/table-array-2d-js/
function makeTable(data, tableId){
    // 表の作成開始
    var cell='';
    var rows=[];
    var table = document.createElement("table");

    // 表に2次元配列の要素を格納
    for(var i = 0; i < data.length; i++){
        rows.push(table.insertRow(-1));  // 行の追加
        for(var j = 0; j < data[0].length; j++){
            cell=rows[i].insertCell(-1);
            // nullの置換
            if(data[i][j] === null){
                data[i][j] = '-';
            }
            cell.appendChild(document.createTextNode(data[i][j]));
            cell.style.border = "1px solid gray"; // 枠線
            // 背景色の設定
            if(i==0){
                cell.style.backgroundColor = "#bbb"; // ヘッダ行
            }else{
                //cell.style.backgroundColor = "#ddd"; // ヘッダ行以外
            }
            //変動比の色の設定
            if(i!=0 && (j==5 || j==6)){
                if((j==5 && data[i][j] < 0) || (j==6 && data[i][j].indexOf('-') != -1) ){
                    cell.style.color = "red";    // マイナス
                }else{
                    cell.style.color = "green";  // プラス
                }
            }
        }
    }
    // 指定したdiv要素に表を加える
    document.getElementById(tableId).appendChild(table);
}

function updateTable(data, tableId){
    var table = document.getElementById(tableId).lastChild; //bug fix:firstChild->lastChild
    var text_value = '';

    // 表の値を更新
    for(var i = 0; i < data.length; i++){
        for(var j = 0; j < data[0].length; j++){
            text_value=table.rows[i].cells[j].firstChild;
            // nullの置換
            if(data[i][j] === null){
                data[i][j] = '-';
            }
            text_value.nodeValue = data[i][j];
            //変動比の色の設定
            if(i!=0 && (j==5 || j==6)){
                if((j==5 && data[i][j] < 0) || (j==6 && data[i][j].indexOf('-') != -1) ){
                    table.rows[i].cells[j].style.color = "red";    // マイナス
                }else{
                    table.rows[i].cells[j].style.color = "green";  // プラス
                }
            }
        }
    }
}
// ]]></script>

解説

 ページ読み込み時に下記の処理を実行する。

 1.初期処理(InitProc)

  グローバル変数の初期設定を行う。

 2.メイン処理(MainProc)

    (1)表のヘッダー部分の編集

    (2)getCryptoInfo関数で通貨情報を取得し、editInfo関数に渡す。

    (3)editInfo関数で表のデータ部分を編集し、テーブルを作成、又は更新する。

 3.自動更新処理(AutoUpdate)

        setInterval関数でメイン処理を10秒ごとに実行する。

補足事項

使用しているAPI

IEX APIの「Crypto」を使用しています(下記リンクからCryptoの項目に飛べます)。
API 1.0 | IEX #crypto

 
表示している通貨はIEX APIで取得できるものを表示しています。
Crypto #402

各項目の説明

一覧の各項目の説明は「Quote」の項目を参照(下記リンクからQuoteの項目に飛べます)。
API 1.0 | IEX #quote

一覧には、APIで取得した情報の一部のみを表示しています。
以下のようなJSONデータを取得できるので、その内の必要なものを表に編集して表示しています。
{"symbol":"BTCUSDT",
companyName:"Bitcoin USD",
primaryExchange:"crypto",
sector:"cryptocurrency",
calculationPrice:"realtime",
open:3860.6,
openTime:1543255545323,
close:3815.03795516,
closeTime:1543341945323,
high:3987,
low:3689.12,
latestPrice:3795.06,
latestSource:"Real time price",
latestTime:"1:05:45 PM",
latestUpdate:1543341945323,
latestVolume:77984.561363,
iexRealtimePrice:null,
iexRealtimeSize:null,
iexLastUpdated:null,
delayedPrice:null,
delayedPriceTime:null,
extendedPrice:null,
extendedChange:null,
extendedChangePercent:null,
extendedPriceTime:null,
previousClose:3862.27,
change:-65.54,
changePercent:-0.01698,
iexMarketPercent:null,
iexVolume:null,
avgTotalVolume:null,
iexBidPrice:null,
iexBidSize:null,
iexAskPrice:null,
iexAskSize:null,
marketCap:null,
peRatio:null,
week52High:null,
week52Low:null,
ytdChange:null,
bidPrice:3793.64,
bidSize:2.412389,
askPrice:3796.28,
askSize:0.015882}

参考記事

APIの呼び出し等

JavaScriptでローソク足チャートの作成 - Qiita

JavaScriptでの表の作成

下記記事の「makeTable」関数をほぼそのまま使用させて頂きました。変更点は枠線の設定、 nullの置換、変動比の色の設定等です。
【Javascript】表(table)の動的作成

表示形式

仮想通貨レート・相場 時価総額順【リアルタイム更新】 - みんなの仮想通貨
Cryptocurrency Screener - Yahoo Finance

自動更新

一定時間で繰り返す(setInterval)-JavaScript入門

テーブル操作など

二章第八回 テーブルの操作 — JavaScript初級者から中級者になろう

課題

APIの呼び出し

下記のURLであれば全ての通貨情報を一度に取得できるが、エラーが頻発するためやむをえず1通貨ずつ取得する形式にした。
https://api.iextrading.com/1.0/stock/market/crypto

都合、一度の更新で18回APIをcallする作りになってしまった。
これ何とかできないかな…

通常のHTMLとはてなブログでの差異

通常のHTMLとはてなブログで差が生じる意味がわからなかった。
詳細は下記記事の「追記:バグ修正」に記載。

【プログラミング】IEX APIで暗号通貨のレート情報を一覧表示【JavaScript】

お分かりの方がいらっしゃれば、ご教示頂けると助かりますm(_ _)m

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

JavaScriptのforEachで同期的にawaitが使えない問題の対処法

こんにちは、とくめいチャットサービス「ネコチャ」運営者のアカネヤ(@ToshioAkaneya)です。

JavaScriptのforEachでawaitが使えない問題とは、次のような問題です。

const arr = [1, 2, 3];

arr.forEach(async (el)=>{
  await someFunction(el)
})

これは、someFunctionを同期的に3回呼び出そうとして書いたコードです。
しかし実際に実行すると分かるのですが、
someFunction(1)
someFunction(2)
someFunction(3)
はそれぞれの完了を待たずに一斉に呼び出されています。
これはforEachメソッドの性質ですので、どうすることもできません。

JavaScriptのforEachで同期的にawaitが使えない問題の対処法

for of文を使い、全体をasync関数でラッピングすることでこの問題は解決できます。

const arr = [1, 2, 3];
(async(){
  for(let el of arr){
    await someFunction(el)
  }
})()

はてなブックマーク・Pocketはこちらから

はてなブックマークに追加
Pocketに追加

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

URLのクエリパラメタを取得する(IE11対応)

やりたいこと

現在の表示しているwebページのURLからクエリパラメタを取得したいとします。

コード例は ↓にアクセスしたときの結果です。
https://example.com?hello=こんにちは&bye=さようなら
CodePen置いておきます。https://codepen.io/anon/pen/YBdvye

方法1: モダンブラウザの場合

URL#searchParams を使います。

const params = (new URL(window.location)).searchParams;
console.log(params.get('hello')); // "こんにちは"
console.log(params.get('bye'));   // "さようなら"
console.log(params.get('hoge'));  // null

返却されるのは plaing Object ではなく URLSearchParams なので、getメソッドを通してパラメタ値を取得します。

↓ここにあるように、IE11では利用できません。
https://developer.mozilla.org/en-US/docs/Web/API/URL#Browser_compatibility

方法2: Internet Explorer 11 (IE11) の場合

URI.js を使います。

ライブラリの取得

使い方

var uri = new URI(window.location);
var params = uri.query(true);
console.log(params['hello']); // "こんにちは"
console.log(params['bye']);   // "さようなら"
console.log(params['hoge']);  // undefined

URI#query の引数を true にすると Object 型で返ってきます。それ以外はクエリパラメタの文字列そのもの(hello=こんにちは&bye=さようなら)が返ってきます。
http://medialize.github.io/URI.js/docs.html#accessors-search

不採用

  • split('&') とか使う・正規表現を使う
    • イレギュラーなケースを考えたくないので不採用
  • purl.js
    • README.md にあるように、メンテナンスされていません
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ブレークポイントを貼らずにアニメーション処理などの途中で画面を一時停止させる方法

概要

ビルドされたファイルが難読化されてたりすると該当部分がよくわからないことがあり、ブレークポイントを貼るのに時間がかかったりする。

そんなときはアニメーション処理のコードの途中に

debugger;

を書けばGoogle Chromeのデベロッパーツールが画面を停止させてくれる。アニメーション中の細かい位置などを確認したいときにおすすめ。

出典

コードをステップ実行する方法  |  Tools for Web Developers  |  Google Developers

onClick が呼び出されるたびに、f という関数が呼び出され、スクリプトは強制的に debugger キーワードで一時停止されます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

指定した何れかのURLパラメータのパターンにマッチしていることを検証するスクリプト

Webサイトへのアクセスがあったとき、
URL内のGETパラメータがこちらの指定したパターンの何れかにマッチすればTRUEを返すスクリプト。
変数TARGET_SEARCH_PATTERNに目的のパラメータの組み合わせを入力することによって使用する。

使用用途

  • 特定のGETパラメータを持ってWebサイト訪問したユーザーを識別してポップアップを表示する
  • 特定のGETパラメータを持ってWebサイト訪問したユーザーにCookieを付与し、計測対象から除外する

コード

(function(){
  // TARGET_SEARCH_PATTERN で指定したパターン配列の何れかがマッチすればTRUEを返す
  var TARGET_SEARCH_PATTERN = [
    {'ref':/^google$/}, // URLのGETパラメータにref=googleを含んでいるならTRUE
    {'utm_medium':/^email$/,'utm_source':/^premium_.*$/}, // パラメータ"utm_medium"が"premium_"で始まればTRUE
    {'utm_medium':/^cpc$/,'utm_source':/^(google|yahoo)$/},  // パラメータ"utm_medium"が"cpc"でかつ、"utm_source"が"google"か"yahoo"であればTRUE
    {'utm_medium':/^cpm$/,'utm_source':/^facebook$/},
  ];

  var searchDic = getSearchDictionary(location.search);
  var flag = false;
  if(Object.keys(searchDic).length > 0){
    for(var i = 0; i < TARGET_SEARCH_PATTERN.length; i++){
      flag = false;
      for(var query in TARGET_SEARCH_PATTERN[i]){
        if(query in searchDic && hasTargetSearchValue(searchDic[query],TARGET_SEARCH_PATTERN[i][query])){
          flag = true;
        }else{
          flag = false;
          break;
        }
      }
      if(flag){return true;}
    }
  }
  return false;

  function getSearchDictionary(queryString){
    var s = queryString.split('&');
    var sDic = {};
    if(s.length > 0){
      s[0] = s[0].slice(1);
      for(var i = 0; i < s.length; i++){
        if(!(s[i].split('=')[0] in sDic)){
          sDic[s[i].split('=')[0]] = [s[i].split('=')[1]];
        }else{
          sDic[s[i].split('=')[0]].push(s[i].split('=')[1]);
        }
      }
    }
    return sDic;
  }
  function hasTargetSearchValue(qVals,valPattern){
    var flag = false;
    for(var i = 0; i < qVals.length; i++){
      if(valPattern.test(qVals[i])){
        flag = true;
      }
    }
    return flag;
  }
})();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ブラウザを閉じるまでsessionStorageでcheckedの値を保持する

ブラウザを閉じるまでcheckboxのcheckedの値を持っていたい。

index.html
<input type="checkbox" name="q1" data-question="q1-1" class="question_checkbox">
<input type="checkbox" name="q1" data-question="q1-2" class="question_checkbox">
<input type="checkbox" name="q1" data-question="q1-3" class="question_checkbox">
<input type="checkbox" name="q1" data-question="q1-4" class="question_checkbox">

クリック時にセッションを登録
ieでquerySelectorAllで取得した値をforeEachでループできない /(^o^)\
ie10でdataset使ってカスタムデータ属性取得できない /(^o^)\

script.js
var targets = document.querySelectorAll('.question_checkbox');
var checkboxClick = function(){
    var self = this;
    sessionStorage.setItem('input[data-question="' + self.getAttribute('data-question') + '"]', self.checked);
};
Array.prototype.forEach.call(targets, function(e){
    e.addEventListener('click', checkboxClick)
})

ページ読み込み時にセッションを確認して、'true'の場合、checkedをtrueにする。

script.js
var addSessionStorage = function() {
    var session = sessionStorage;
    Object.keys(session).forEach(function(key){
        if(key.indexOf('data-question') !== -1 && this[key] === 'true' && document.querySelector(key) !== null) {
            document.querySelector(key).checked = true;
        }
    }, session)
}

windowをloadしたタイミングで実行。

script.js
window.addEventListener('load', addSessionStorage);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのコンストラクタとかいう闇深いものとprivate

JavaScriptのコンストラクタとかいう闇深いものとprivate

js初心者が勉強中にコンストラクタの変な挙動を見つけて気になったので少し実験をしてみた。

目次

constructorとは

オブジェクトの生成と初期化のための特殊なメソッドです。

要はnewしたときに自動で実行される特別なメソッドです

定義の仕方多すぎる問題

動くパターンと動きそうで動かないパターンがあるのでまずは動くパターン

動くパターン

関数

動くパターン
function A() {
    console.log('class A')
}

console.log(new A)
// class A
// A {}

これはA自体がコンストラクタになっているパターン

クロージャ

動くパターン
var B = function() {
    console.log('class B')
}

console.log(new B)
// class B
// B {}

これもAと同じです

クラス

動くパターン
class D {
    constructor() {
        console.log('class D')
    }
}

console.log(new D)
// class D
// D{}

クラスDにconstructorメソッドを定義するパターン
しかし以下の方法はコンストラクタとして正しく定義できません

動かないパターン

アロー関数

動かないパターン
var C = () => {
    console.log('class C')
}

console.log(new C)
// Uncaught TypeError: C is not a constructor

アロー関数式 は、その名のとおり矢印を使って記述し、function 式より短い構文で同様な内容を記述することができます。なおthis, arguments, super, new.target を束縛しません。また、アロー関数式は、メソッドでない関数に最適で、コンストラクタとして使うことはできません。
アロー関数

関数にconstructorメソッドを定義

動かないパターン
function E() {
    this.constructor = function() {
        console.log('class E')
    }
}

console.log(new E)
// E {constructor: ƒ}

Eのconstructorメソッドは実行されません(なんで)

関数のprototypeにconstructorメソッドを定義

動かないパターン
function F() {
}
F.prototype.constructor = function() {
    console.log('class F')
}

console.log(new F)
// F {}
// __proto__:
// constructor: ƒ ()

この場合もFのconstructorメソッドは実行されません(なんで)

自身のクラスのインスタンス以外を戻り値にできる問題

new演算子について調べるとこんな記述が

コンストラクタ関数が返すオブジェクトが、new 式の結果になります。コンストラクタ関数が明示的にオブジェクトを返さない場合は、ステップ 1 で生成したオブジェクトを代わりに使用します。(通常、コンストラクタは値を返しませんが、通常のオブジェクト生成プロセスをオーバーライドしたい場合はそのようにすることができます)
new

どうやらオブジェクトを返すと通常とは違う動きをするらしい

コンストラクタに戻り値を持たせてみた

戻り値がオブジェクト以外
class G {
    constructor() {
        return 1
    }
}

console.log(new G)
// G {}

オブジェクト以外の場合は戻り値が無視されています

戻り値がオブジェクト
class H {
    constructor() {
        return {hoge:'hoge'}
    }
}

console.log(new H)
// {hoge:'hoge'}

戻り値が返って来る
Hはどこかへ行ってしまった。。。
※配列の場合も同じ挙動になります

メソッドやプロパティはどうなるのか

class I {
    constructor() {
        this.hoge = 'hoge'
        return {}
    }

    fuga() {
        return 'fuga'
    }
}

var i = new I
console.log(i)
// {}
console.log(i.hoge)
// undefined
console.log(i.fuga())
// Uncaught TypeError: i.fuga is not a function

まったく別のオブジェクトが返ってくるのでプロパティやメソッドにアクセスできない
だがプロパティやメソッドを戻り値のオブジェクトに持たせることはできる様子

戻り値のオブジェクトにプロパティやメソッドを持たせる
class J {
    constructor() {
        this.hoge = 'hoge'
        return {
            hoge : this.hoge,
            fuga : this.fuga,
        }
    }

    fuga() {
        return this.hoge
    }
}

var j = new J
console.log(j)
// {hoge: "hoge", fuga: ƒ}
console.log(j.hoge)
// hoge
console.log(j.fuga())
// hoge

上記の例の戻り値のプロパティとメソッドは戻り値のオブジェクト自身が持っていることになっている
つまり戻り値のhogeプロパティはJクラスから分離されている
fugaメソッド内でつかうthisは戻り値のオブジェクトを参照する

継承するとconstructorが壊れる問題

以下の、コンストラクタでオブジェクトを返す親クラスがあったとする

親クラス
class Parent {
    constructor() {
        return {}
    }
}

このParentクラスを継承して新しいクラスをつくる
そしてコンストラクタ内でプロパティを定義する

子クラス
class Child extends Parent {
    constructor() {
        super()
        console.log(this)
        this.hoge = 'hoge'
    }
}

console.log(new Child)
// {}
// {hoge:'hoge'}

コンストラクタ内の三行に注目していただきたい

1行目
この行は親クラスのコンストラクタを実行している
これがないとエラーになる

Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

thisを使いたければスーパークラスのコンストラクタを実行しろ的な

2行目
1行目のおかげでthisが使えるようになったので確認してみると中身は親のコンストラクタが返した空のオブジェクト。
つまり一行目ではvar this = super()のようなことがされている感じ

3行目
2行目から分るとおりthisは親のコンストラクタが返した空のオブジェクトなのでChildクラスにプロパティを定義することはできない
Childクラスに他のメソッドを定義してあっても呼び出すことはできない

このようにコンストラクタでオブジェクトを返すと継承先のコンストラクタを汚染することになる

これを使ってなにができるか

privateを実装することができるぽい?

基本概念

Bridgeパターンのような(Bridgeパターンをよく分かってない。。。)橋渡しをしてくれるオブジェクトを、コンストラクタで返す
ただしそのままオブジェクトにプロパティやメソッドを定義していってもthisを指すものがコンストラクタで返すオブジェクト自身になってしまうため工夫をする

メソッド

bind()を使ってthisが何を指すのかを指定する

...
    constructor() {
        this.name = 'hoge'
        return {
            getName : this.getName.bind(this)
        }
    }

    getName() {
        return this.name
    }
...

これでgetName内で、コンストラクタの戻り値で返していないthis.nameを指せるようになった

これがコンストラクタでprivateを実装する基本的な方法です
ようはpublicにしたいものを定義していく方法です

しかしこのままではasyncメソッドは定義できない
なので改造

...
    constructor() {
        this.name = 'hoge'
        return {
            getName : function() {
                return this.getName()
            }.bind(this)
        }
    }

    async getName() {
        return this.name
    }
...

これでasyncメソッドでも実行結果を返すので問題ない(はず)

プロパティ

安直に以下のようにすると正しく動かない

...
    constructor() {
        this.name = 'hoge'
        return {
            name : this.name
        }
    }
...

this.nameを格納しているので、外から値を変更できない
なので改造

...
    constructor() {
        this.name = 'hoge'
        let bridge = {}
        Object.defineProperty(bridge, 'name ', {
            get: function() {
                return this.name ;
            }.bind(this),
            set: function(arg) {
                this.name = arg
            }.bind(this)
        })
        return bridge
    }
...

Object.definePropertyを使ってコンストラクタで返すオブジェクトにgetter setterメソッドを定義してbind()する

これで外でプロパティを変更しても反映される

継承してコンストラクタを使いたいときの救済措置

この方法でprivateを表現するとコンストラクタが壊れてしまう
これはどうしようもないです
なので救済措置

コンストラクタ内のthisが汚染されるだけなので、_init_()メソッド(名前はなんでもいいです)を別に用意してこれをコンストラクタ代わりにする

可変長引数で引数の数は子クラスに自動であわせられる

親クラス
...
    constructor(...initArgs) {
        this._init_(...initArgs)
        return {
            ...
        }
    }

    _init_() {}
...
子クラス
...
    _init_(argA, argB) {
        super._init_()
        this.hoge = argA
        this.fuga = argB
    }
...

でもpublicで返したいものは全部親クラスがなんとかしないといけない
継承したら子クラスのプロパティやメソッドpublicにできないよね...
親クラスが子クラスの実装内容知ってたら気持ち悪いし
ていうかいちいちpublicなやつ定義するのめんどくさいよね...

だから全部自動でpublicなやつ定義する

最終結果

簡単にprivateできるクラスが作れます

使い方

  • 下記のクラスを継承する
  • 一文字目が_、二文字目が_以外のプロパティとメソッドはprivateになる
    • this.hogeはパブリック
    • this._hogeはプライベート
  • 初期化処理は_init_()を使う

コード

class Base {
    constructor(...initArgs) {
        // 継承先用の独自コンストラクタ
        this._init_(...initArgs)

        // コンストラクタで返すオブジェクト
        let bridge = {}

        // プロパティとメソッド全部取得
        let obj = this
        let propNames = []
        while (obj) {
            propNames = propNames.concat(Object.getOwnPropertyNames(obj))
            obj = Object.getPrototypeOf(obj) // __proto__も全部見る
        }

        // privateのパターン
        let privatePattern = /^_[^_].*/
        // publicにするやつ全部定義する
        for (let propName of new Set(propNames)) {
            if (privatePattern.test(propName) || propName === '__proto__') {
                // privateのパターンにマッチするか__prop__のときはcontinue
                continue
            }

            if (typeof this[propName] === 'function') {
                // 関数の場合
                bridge[propName] = function(...args) {
                    return this[propName](...args)
                }.bind(this)
            } else {
                // プロパティ(getterも含む)
                Object.defineProperty(bridge, propName, {
                    get: function() {
                        return this[propName];
                    }.bind(this),
                    set: function(arg) {
                        this[propName] = arg
                    }.bind(this)
                })
            }
        }
        return bridge
    }

    _init_() {}
}

問題点

  • このクラスを継承しなければならない
  • コンストラクタが汚染されるので代わりの_init_()を使わなければならない
  • privateなstaticメソッドは定義できない
  • インスタンスした結果をconsole.logしてもどのクラスなのか分りにくい
    • メソッドの[[BoundThis]]を見れば一応分る
  • インスタンスするたびにオブジェクトつくるのでたくさんインスタンスするのは重そう
    • bridgeをstaticプロパティにしてうまくディープコピーできればよさげ(やり方教えてください)
  • IEは知らないです

最後に

js難しいです。
間違っていることや足りないことがあれば教えていただけると嬉しいです
__init__()はPythonのコンストラクタの名前をもらいましたが隠蔽したほうがよさそうなので_init_()とかのほうがいいかもです
_init_()にしました

全然privateじゃありませんでしたこれprotectedでした^^

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

Vueでダイアログを動的に生成してマウントするサンプル作ってみた

課題

Vueでサービスを作っててダイアログやモーダルを表示したいときに、公式だとdataにフラグを持たせてClickイベントでtrue/falseを切り替えるようにしていた。ただ、これだと画面内で表示したいダイアログが増えるほどにdataのフラグも増えるし、テンプレートの中に各フラグと紐づいたdialogタグを量産することになって、めちゃくちゃ見にくいし何より格好悪い。
なので、showDialog()的な感じでメソッド呼び出しでダイアログを表示させたい

やったこと

renoinn/vue-dialog-sample
というわけで、DialogHelper.showDialog()とすることでダイアログを表示させるサンプルを作ってみた。

解説

キモになるのは以下の点

Dialog.vue
methods: {
    attach () {
        if (!this.$parent) {
            this.$mount();
            document.body.appendChild(this.$el);
        } else {
            this.$mount();
            this.$parent.$el.appendChild(this.$el);
        }
    },
    remove () {
        if (!this.$parent) {
            document.body.removeChild(this.$el);
            this.$destroy();
        } else {
            this.$parent.$el.removeChild(this.$el);
            this.$destroy();
        }
    },
    close () {
        this.isShow = false;
    },
    show () {
        this.attach();
        this.isShow = true;
    },
    afterLeave () {
        this.remove();
    }
}

まずDialog.vue側で自身をappendChildしたり、removeChildするメソッドを用意してやる。この時単にdocument.body.appendChild(this.$el);とすると、Vueの仮想DOMツリーから外れてしまって、例えばvue-routerを使ってて画面遷移してもダイアログが消えないとか問題が起きてしまう。
なので、this.$parentを見て、設定されていればその下にappendChildするようにする。

DialogHelper.js
import Vue from 'vue';
import Dialog from '@/components/Dialog.vue';

const DialogHelper = {
    showDialog (context, { subject, message, ok, cancel }) {
        let DialogVM = Vue.extend(Dialog);
        let vm = new DialogVM({
            parent: context,
            propsData: {
                subject: subject,
                message: message,
                onPrimary () {
                    ok();
                    vm.close();
                },
                onSecondary () {
                    cancel();
                    vm.close();
                }
            }
        });
        vm.show();
    }
}
export default DialogHelper;

次にダイアログを生成するHelperを用意する。Vueのコンポーネントは単一コンポーネントファイルであっても、Vue.extend(Component);とすることで、ソースコード上でクラスとして使用できるようになる。
メソッドの引数でcontextを受け取って、コンストラクタでparent: contextとしてやることで、上述のthis.$parentを設定できるようにしておく。

<script>
import DialogHelper from '@/DialogHelper';

export default {
  methods: {
    showDialog () {
      DialogHelper.showDialog(this, {
        subject: 'Subject',
        message: 'open temporary dialog sample',
        ok: () => { console.log('click ok') },
        cancel: () => { console.log('click cancel') }
      });
    }
  }
}
</script>

呼び出しはHelperをimportして、showDialogから呼び出す。第一引数でthisを渡しているので、呼び出したコンポーネントの下にDialogコンポーネントがappendされることになる。
thisじゃなくて、this.$rootthis.$parentを渡すこともできる。

今回はダイアログだったけど、同じような感じでモーダルやトーストも実装できる。

参考URL

https://qiita.com/hako1912/items/8c0462203987f2cd15b1
https://kitak.hatenablog.jp/entry/2017/04/04/044829
https://github.com/paliari/v-slim-dialog

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

[##]Paper.jsを始めてみよう 解説まとめ

こんにちは。yokuneru_gsです。

本連載ではPaper.jsというJavaScriptライブラリの解説を公式チュートリアルを参考に行なっています。
Paper.jsとはHTML5におけるCanvasでの図形描画を便利にするオープンソースのライブラリです。


以下にこれまでの記事をまとめたので、indexとしてご利用ください。

[#0]Paper.jsを始めてみよう -Paper.jsとは何か-

[#1]Paper.jsを始めてみよう -Paper.jsを動かしてみよう-

[#2]Paper.jsを始めてみよう -Point, Size, Rectangleを理解しよう-

[#3]Paper.jsを始めてみよう -オブジェクトの変更を理解しよう-

[#4]Paper.jsを始めてみよう -数学的な演算処理を理解しよう-

[#5]Paper.jsを始めてみよう -ベクトルの扱いについて理解しよう1-

[#6]Paper.jsを始めてみよう -ベクトルの扱いについて理解しよう 2-


現在公開中の記事は以上です。
随時更新していきますので、よろしくお願います。

*Twitterもフォローして頂けると幸いです。
こちら→@yokuneru_gs

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

[##]Paper.jsを始めてみよう 解説一覧

こんにちは。yokuneruです。

本連載ではPaper.jsというJavaScriptライブラリの解説を公式チュートリアルを参考に行なっています。
Paper.jsとはHTML5におけるCanvasでの図形描画を便利にするオープンソースのライブラリです。

チュートリアルを実際に触ってみて、私がつまづいた所はできるだけわかりやすく書き換えています。
もし、わからないことなどあればご連絡ください。


以下にこれまでの記事をまとめたので、indexとしてご利用ください。

[#0]Paper.jsを始めてみよう -Paper.jsとは何か-

[#1]Paper.jsを始めてみよう -Paper.jsを動かしてみよう-

[#2]Paper.jsを始めてみよう -Point, Size, Rectangleを理解しよう-

[#3]Paper.jsを始めてみよう -オブジェクトの変更を理解しよう-

[#4]Paper.jsを始めてみよう -数学的な演算処理を理解しよう-

[#5]Paper.jsを始めてみよう -ベクトルの扱いについて理解しよう1-

[#6]Paper.jsを始めてみよう -ベクトルの扱いについて理解しよう 2-

[#7]Paper.jsを始めてみよう -パスの扱いについて理解しよう-


現在公開中の記事は以上です。
随時更新していきますので、よろしくお願います。

*Twitterもフォローして頂けると幸いです。
こちら→@yokuneru_gs

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

動的に追加した要素のイベントを拾う

動的に追加した要素のイベントが拾えませんと相談されたのでサンプルを書きました.

例えば「行追加」ボタンを押したらテーブルに行が追加される場合です.
最初に表示されている行の「イベント発火!」ボタンを押すとalertが出ますが,追加した行のボタンを押してもイベントが拾えません.

<table>
  <tr>
    <th></th>
  </tr>
  <tr>
    <td>
      <button type="button" class="alert-btn">イベント発火!</button>
    </td>
  </tr>
</table>
<br />
<button type="button" id="addRow">行追加</button>
$(function(){
  var tr = '<tr><td><button type="button" class="alert-btn">イベント発火!</button></td></tr>';
  $('#addRow').click(function(){
    $('table').append(tr);
  });

  $('.alert-btn').click(function(){
    alert('ボタンがクリックされました');
  });
});

See the Pen cannot handle added row event by mt (@mtakeda) on CodePen.

そこでbodyのイベントを拾うようにすると動的に追加した要素のイベントを拾うことができます.

$(function(){
  var tr = '<tr><td><button type="button" class="alert-btn">イベント発火!</button></td></tr>';
  $('#addRow').click(function(){
    $('table').append(tr);
  });

  $('body').on('click', '.alert-btn', function(event){
    alert('ボタンがクリックされました');
  });
});

See the Pen handle added row event by mt (@mtakeda) on CodePen.

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

[#6]Paper.jsを始めてみよう -ベクトルの扱いについて理解しよう 2-

こんにちは。yokuneruです。

前回の[#5]-ベクトルの扱いについて理解しよう1-に引き続き、Paper.jsの解説をしていきます。

今回は公式チュートリアルのVector Geometryを参考に解説していきます。

ベクトルの回転と角度の操作

ベクトルを回転させることはパスや形状を構築するための強力なツールです。例えば、あるベクトルから一定の角度を回転させたベクトルを定義することを可能にします。
[マウスベクトルの操作チュートリアル(後日公開)]ではベクトルの回転を利用して、移動したマウスの方向と平行にパスを作成するサンプルを紹介します。

Paper.jsでは水平軸から時計回りに角度が計測され、180°で値が-180°に反転されます。角度を180°以上に設定することも可能です。

Angles.gif

ベクトルの角度を変えるには2つの方法があります。1つはベクトルのangleプロパティに新たな値を設定することです。

以下の例ではx = 100, y = 100のベクトルを用いてみます。

1つ目は直接角度を指定する方法です。
当初のベクトルの角度45°に対して、angleプロパティでベクトルの角度を135°に変更します。
この時、ベクトルの値はx = -100, y = 100に変換されます。

var vector = new Point(100, 100);
console.log(vector.angle); // 45
console.log(vector.length); // 141.42136

vector.angle = 135;
console.log(vector.angle); // 135
console.log(vector); // {x: -100, y: 100}

Rotating-Vectors-02.gif

2つ目の方法はベクトルの角度に数値を足し合わせる方法です。
以下の例ではangleプロパティを用いて、現在の角度(45°)に90°足し合わせています。

vector.angle += 90;
console.log(vector.angle); // 135
console.log(vector); // {x: -100, y: 100}

Rotating-Vectors-03.gif

どちらの方法でも同じ結果を得ることができますが、ベクトルの長さは変わらないことに注意してください。

メソッドとプロパティの操作

四則演算やrotate(),normalize()などのメソッドはベクトルやpointなどを直接変更せず、新たなオブジェクトとして結果を返す点に注意してください。以下の例のように演算やメソッドをつなげて使用します。

<!DOCTYPE html>
<html>
<head>
  <!-- Load the Paper.js library -->
  <script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.0/paper-full.js"></script>
  <!-- Define inlined PaperScript associate it with myCanvas -->
  <script type="text/paperscript" canvas="myCanvas">

  var segments = [new Point(50, 50), new Point(150, 150)];
  var path = new Path(segments);
  path.strokeColor = "red";
  var rotatedPath = path.rotate(90);
  rotatedPath.strokeColor = "blue";

  </script>
</head>
<body>
  <canvas id="myCanvas" style="border: 1px solid black" height="200"></canvas>
</body>
</html>

スクリーンショット 2019-02-18 12.49.39.png
上記のコードではこのような結果が得られ、元の赤線が描画されないことがわかります。

一方、ベクトルの角度や長さを変更すると、ベクトルオブジェクトが直接編集されます。そのため、元のオブジェクトを変更したくない場合には以下のようにclone()関数を使用してください。

  var segments = [new Point(50, 50), new Point(150, 150)];
  var path = new Path(segments);
  path.strokeColor = "red";

  var clonedPath = path.clone();
  var rotatedPath = clonedPath.rotate(90);
  rotatedPath.strokeColor = "blue";

スクリーンショット 2019-02-18 12.53.26.png
元の赤線が残された上で、新たに90°回転した青線が描画されていることがわかります。
また、rotateメソッドはroteta(angle, (center))で表されるので、オプションで回転軸となるcenterの値を指定することが可能です。

Vector.js

以下のサンプルコードはベクトルの概念を理解してもらうために用意されたものです。
これを使ってこれまでのチュートリアルの理解を深めてください。

<!DOCTYPE html>
<html>
<head>
  <!-- Load the Paper.js library -->
  <script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.0/paper-full.js"></script>
  <!-- Define inlined PaperScript associate it with myCanvas -->
  <script type="text/paperscript" canvas="myCanvas">

  var values = {
    fixLength: false,
    fixAngle: false,
    showCircle: false,
    showAngleLength: true,
    showCoordinates: false
  };

  var vectorStart, vector, vectorPrevious;
  var vectorItem, items, dashedItems;

  function processVector(event, drag) {
    vector = event.point - vectorStart;
    if (vectorPrevious) {
        if (values.fixLength && values.fixAngle) {
            vector = vectorPrevious;
        } else if (values.fixLength) {
            vector.length = vectorPrevious.length;
        } else if (values.fixAngle) {
            vector = vector.project(vectorPrevious);
        }
    }
    drawVector(drag);
  }

  function drawVector(drag) {
    if (items) {
        for (var i = 0, l = items.length; i < l; i++) {
            items[i].remove();
        }
    }
    if (vectorItem)
        vectorItem.remove();
    items = [];
    var arrowVector = vector.normalize(10);
    var end = vectorStart + vector;
    vectorItem = new Group([
        new Path([vectorStart, end]),
        new Path([
            end + arrowVector.rotate(135),
            end,
            end + arrowVector.rotate(-135)
        ])
    ]);
    vectorItem.strokeWidth = 0.75;
    vectorItem.strokeColor = '#e4141b';
    // Display:
    dashedItems = [];
    // Draw Circle
    if (values.showCircle) {
        dashedItems.push(new Path.Circle({
            center: vectorStart,
            radius: vector.length
        }));
    }
    // Draw Labels
    if (values.showAngleLength) {
        drawAngle(vectorStart, vector, !drag);
        if (!drag)
            drawLength(vectorStart, end, vector.angle < 0 ? -1 : 1, true);
    }
    var quadrant = vector.quadrant;
    if (values.showCoordinates && !drag) {
        drawLength(vectorStart, vectorStart + [vector.x, 0],
                [1, 3].indexOf(quadrant) != -1 ? -1 : 1, true, vector.x, 'x: ');
        drawLength(vectorStart, vectorStart + [0, vector.y],
                [1, 3].indexOf(quadrant) != -1 ? 1 : -1, true, vector.y, 'y: ');
    }
    for (var i = 0, l = dashedItems.length; i < l; i++) {
        var item = dashedItems[i];
        item.strokeColor = 'black';
        item.dashArray = [1, 2];
        items.push(item);
    }
    // Update palette
    values.x = vector.x;
    values.y = vector.y;
    values.length = vector.length;
    values.angle = vector.angle;
  }

  function drawAngle(center, vector, label) {
    var radius = 25, threshold = 10;
    if (vector.length < radius + threshold || Math.abs(vector.angle) < 15)
        return;
    var from = new Point(radius, 0);
    var through = from.rotate(vector.angle / 2);
    var to = from.rotate(vector.angle);
    var end = center + to;
    dashedItems.push(new Path.Line(center,
            center + new Point(radius + threshold, 0)));
    dashedItems.push(new Path.Arc(center + from, center + through, end));
    var arrowVector = to.normalize(7.5).rotate(vector.angle < 0 ? -90 : 90);
    dashedItems.push(new Path([
            end + arrowVector.rotate(135),
            end,
            end + arrowVector.rotate(-135)
    ]));
    if (label) {
        // Angle Label
        var text = new PointText(center
                + through.normalize(radius + 10) + new Point(0, 3));
        text.content = Math.floor(vector.angle * 100) / 100 + '°';
        text.fillColor = 'black';
        items.push(text);
    }
  }

  function drawLength(from, to, sign, label, value, prefix) {
    var lengthSize = 5;
    if ((to - from).length < lengthSize * 4)
        return;
    var vector = to - from;
    var awayVector = vector.normalize(lengthSize).rotate(90 * sign);
    var upVector = vector.normalize(lengthSize).rotate(45 * sign);
    var downVector = upVector.rotate(-90 * sign);
    var lengthVector = vector.normalize(
            vector.length / 2 - lengthSize * Math.sqrt(2));
    var line = new Path();
    line.add(from + awayVector);
    line.lineBy(upVector);
    line.lineBy(lengthVector);
    line.lineBy(upVector);
    var middle = line.lastSegment.point;
    line.lineBy(downVector);
    line.lineBy(lengthVector);
    line.lineBy(downVector);
    dashedItems.push(line);
    if (label) {
        // Length Label
        var textAngle = Math.abs(vector.angle) > 90
                ? textAngle = 180 + vector.angle : vector.angle;
        // Label needs to move away by different amounts based on the
        // vector's quadrant:
        var away = (sign >= 0 ? [1, 4] : [2, 3]).indexOf(vector.quadrant) != -1
                ? 8 : 0;
        value = value || vector.length;
        var text = new PointText({
            point: middle + awayVector.normalize(away + lengthSize),
            content: (prefix || '') + Math.floor(value * 1000) / 1000,
            fillColor: 'black',
            justification: 'center'
        });
        text.rotate(textAngle);
        items.push(text);
    }
  }

  var dashItem;

  function onMouseDown(event) {
    var end = vectorStart + vector;
    var create = false;
    if (event.modifiers.shift && vectorItem) {
        vectorStart = end;
        create = true;
    } else if (vector && (event.modifiers.option
            || end && end.getDistance(event.point) < 10)) {
        create = false;
    } else {
        vectorStart = event.point;
    }
    if (create) {
        dashItem = vectorItem;
        vectorItem = null;
    }
    processVector(event, true);
  //    document.redraw();
  }

  function onMouseDrag(event) {
    if (!event.modifiers.shift && values.fixLength && values.fixAngle)
        vectorStart = event.point;
    processVector(event, event.modifiers.shift);
  }

  function onMouseUp(event) {
    processVector(event, false);
    if (dashItem) {
        dashItem.dashArray = [1, 2];
        dashItem = null;
    }
    vectorPrevious = vector;
  }

  </script>
</head>
<body>
    <canvas id="myCanvas" width="500" height="500" style="border: 1px solid black"></canvas>
</body>
</html>

スクリーンショット 2019-02-18 12.04.32.png

このような画面が表示されれば描画成功です。
ドラッグ&ドロップするとそれに応じた矢印が描画されます。


「ベクトルの扱いについて理解しよう 2」は以上です。
続きは[#7]-パスの扱いについて理解しよう-です。

また、本連載の一覧ページもありますので、そちらもぜひご覧ください。

*Twitterもフォローして頂けると幸いです。
こちら→@yokuneru_gs

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

Jestで AWS SDKモジュールをモック化する方法(promise込み)

Jestでモック化できるケース・できないケース

普通にモック化できるケース

通常。関数やモジュールは、requireして使用していれば、モックオブジェクトを差し込むことができる。

module_a.js
module.exports = {
  featureA (paramA, ParamB) => {
    ...
  },
module.spec.js
const moduleA = require('module_a')

describe('module a test suite', () => {
  test('exist cognito user', async () => {
    moduleA.featureA = jest.fn()
  ...

モック化できないケース(AWS SDKを使用)

ここで、AWS SDKを以下のようにrequireしているとします。

ここでは、例として CognitoIdentityServiceProvider を使用しています。

わりとこのような使い方が普通かと思います。

module_a.js
const AWS = require('aws-sdk')
const cognitoidp = new AWS.CognitoIdentityServiceProvider()

module.exports = {
  featureA async (paramA, ParamB) => {
    await cognitoidp.adminGetUser(param).promise()
    ...
  },

上記で、cognitoidp.adminGetUserと、そのpromise()をモック化したいとします。

これを以下のように記述しても意図した通りになりません。

module.spec.js
const moduleA = require('module_a')

const AWS = require('aws-sdk')
const cognitoidp = new AWS.CognitoIdentityServiceProvider()

describe('module a test suite', () => {
  test('exist cognito user', async () => {
    cognitoidp.adminGetUser = jest.fn()
      ... // mockReturnValueとかを書く
  ...

モジュール側でもテスト側でも、AWS SDKのインスタンスをnewしているため、上手くモック化できません。

AWS SDKモジュールのモック化

AWS SDKのモジュールや特定のメソッドをモック化するには、
「自前で作成しているモジュールにてエクスポートし、テスト側でそれを利用する」
という方法をとる。

module_a.js
const AWS = require('aws-sdk')
const cognitoidp = new AWS.CognitoIdentityServiceProvider()

module.exports = {
  cognitoidp,
  featureA async (paramA, ParamB) => {
    await cognitoidp.adminGetUser(param).promise()
    ...
  },

テスト側からは、上記モジュールをrequireした上で、cognitoidpの中で必要なメソッドに jest.fn()を差し込む。
cognitoidp.adminGetUser をモック化する例を記載する。
本記事では、AWS SDKをpromise込みでコールしている。
このため、moduleA.cognitoidp.adminGetUserのモック化と、返却オブジェクトのメソッドであるpromiseのモック化をそれぞれ行なっている。
async/awaitを使用する場合は以下のようにする必要があるはず。

module.spec.js
const moduleA = require('module_a')

test('exist cognito user', async () => {
    moduleA.cognitoidp.adminGetUser = jest.fn()
      .mockReturnValue({
        promise: jest.fn()
          .mockResolvedValue({
            UserAttributes: [
              {
                attr: "value"
              }
            ]
          })
      })
  ...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クリックした次の要素をトグルする

Goal

クリックしたshowboxの次の要素(hiddenbox)を表示し、もう一度クリックしたら非表示にする。

How to

test.html
<div class="showbox">
  ここをクリック
</div>

<div class="hiddenbox">
  ここが表示されたり、消えたりする
</div>

<script>
    $(".showbox").click(function(){
        $(this).next(".hiddenbox")
        $('.hiddenbox').toggle(2000);
        $('.hiddenbox').siblings('.hiddenbox').slideUp();
    });
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

癖あるNaNの判別方法

NaN === NaN

先日入力されて文字列を数値に変換するために parseInt()を使用しまして、その際parseIntは最初の文字が数値に変換できない場合 NaNを返すのでそれを利用して数値と数値以外を分けるif文を書こうとしたんですが

サンプル
const test = prompt('数値を入力してくだい');
const number = parseInt(test);
if(number === NaN){
  alert('不正な値');
} else {
  alert('正常な値');
}

サンプルコード(Codepen)
こんな感じでif文に 変換された値 === NaNという条件式を書いたのですが、実行してみたら全ての文字が正常な値として認識されました、キャンセル押しても正常な値として認識されます。

ちょっと調べてみたら NaN === NaNだと falseと判断されるらしく、 こういう時はisNaNという関数を使うということがわかりました。

isNaN関数を使う

サンプル
const test = prompt('数値を入力してくだい');
const number = parseInt(test);
if(isNaN(number){
  alert('不正な値');
} else {
  alert('正常な値');
}

サンプルコード

数値以外を入力したら不正な値として認識されるようになりました。


しかし、isNaNにはとある問題があるということがわかりました。

isNaNの問題点

isNaNは、引数が数値ではなかった場合、まず暗黙の型変換によって数値に変換されてしまうようで、以下の記述だとおかしな結果になってしまいます

isNaNの判別
console.log(isNaN(NaN)); //true
console.log(isNaN(1)); //false
console.log(isNaN('NaN')); //true
console.log(isNaN()); //true

サンプルコード
isNaN関数は「数値であることをチェックする関数」ではなく「数値でないことをチェックする関数」であると考えられているようです。

このような問題を解決するためには、 Number.isNaNを使うようです。

Number.isNaN

Number.isNaN
console.log(Number.isNaN(NaN)); //true
console.log(Number.isNaN(1)); //false
console.log(Number.isNaN('NaN')); //false
console.log(Number.isNaN()); //false

サンプルコード

これで、きちんとNaNの時だけtrueが返ってくるようになりました。

しかし、Number.isNaNは、比較的新しく定義されたもの(ECMAScript6から)らしく、ブラウザによっては対応されてないのものもあるそうです、そういう時は、自分でNumber.isNaNを作る必要があります

Number.isNaNを作る

サンプル
Number.isNaN = Number.isNaN || function(value) {
    return typeof value === "number" && value !== value;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

癖あるNaNの判別方法、isNaN関数の問題

NaN === NaN

先日入力された文字列を数値に変換するために parseInt()を使用しまして、その際parseIntは最初の文字が数値に変換できない場合 NaNを返すのでそれを利用して数値と数値以外を分けるif文を書こうとしたんですが

サンプル
const test = prompt('数値を入力してください');
const number = parseInt(test);
if(number === NaN){
  alert('不正な値');
} else {
  alert('正常な値');
}

サンプルコード(Codepen)
こんな感じでif文に 変換された値 === NaNという条件式を書いたのですが、実行してみたら全ての文字が正常な値として認識されました、キャンセル押しても正常な値として認識されます。

ちょっと調べてみたら NaN === NaNだと falseが返ってくるようで、 こういう時はisNaNという関数を使うということがわかりました。

なぜfalseと返ってくるのかは以下のサイトで詳しく解説されていました
NaN === NaN が false な理由とutil.isDeepStrictEqual

isNaN関数を使う

サンプル
const test = prompt('数値を入力してください');
const number = parseInt(test);
if(isNaN(number)){
  alert('不正な値');
} else {
  alert('正常な値');
}

サンプルコード

数値以外を入力したら不正な値として認識されるようになりました。


しかし、isNaNにはとある問題があるということがわかりました。

isNaNの問題点

isNaNは、引数が数値ではなかった場合、まず暗黙の型変換によって数値に変換されてしまうようで、以下の記述だとおかしな結果になってしまいます

isNaNの判別
console.log(isNaN(NaN)); //true
console.log(isNaN(1)); //false
console.log(isNaN('NaN')); //true
console.log(isNaN()); //true

サンプルコード
isNaN関数は「数値であることをチェックする関数」ではなく「数値でないことをチェックする関数」であると考えられているようです。

このような問題を解決するためには、 Number.isNaNを使うようです。

Number.isNaN

Number.isNaN
console.log(Number.isNaN(NaN)); //true
console.log(Number.isNaN(1)); //false
console.log(Number.isNaN('NaN')); //false
console.log(Number.isNaN()); //false

サンプルコード

これで、きちんとNaNの時だけtrueが返ってくるようになりました。

しかし、Number.isNaNは、比較的新しく定義されたもの(ECMAScript6から)らしく、ブラウザによっては対応されてないのものもあるそうです、そういう時は、自分でNumber.isNaNを作る必要があります

Number.isNaNを作る

サンプル
Number.isNaN = Number.isNaN || function(value) {
    return typeof value === "number" && value !== value;
}

こういったブラウザ間の機能を埋めることを「ポリフィル(polyfill)」と言うようです。

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

Element から学ぶ Vue.js の component の作り方 その3 (card)わ

Element-ui とは

第3回になり、今まではあまりにも説明不足だったと反省しました。
Elemnt-ui は Vue.js のコンポーネントライブラリです。
Vue.js で作成されているため、インポートすることで様々なコンポーネントを利用可能になるます。
CSS フレームワークとも捉えることができ、デザイン済みのコンポーネントを簡単に利用することが可能です。
CSSフレームワークなので、ある程度整形されたコンポーネントに変更を加えて、自身のプロダクト色に染めることも可能です。

今回は Elemnt 2.52 がベースになってます。
公式ページ
https://element.eleme.io/#/en-US\

今回は card コンポーネントを解析していきたいと思います。
image.png

ソースの構成は

 index.js
  src
   |- main.vue

となっていいます。

main.vue

main.vue
<template>
  <div class="el-card" :class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'">
    <div class="el-card__header" v-if="$slots.header || header">
      <slot name="header">{{ header }}</slot>
    </div>
    <div class="el-card__body" :style="bodyStyle">
      <slot></slot>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'ElCard',
    props: {
      header: {},
      bodyStyle: {},
      shadow: {
        type: String
      }
    }
  };
</script>

main.vue を見ると、難しいことはしてなさそうです。

  <div class="el-card" :class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'">

上記では class を指定しています。

class="el-card" は固定で指定されます。
shadow は 指定されている場合は is-指定した値 が class になります。
指定されて表示が切り替わるのは always / hover / never のいずれかです。

image.png

:class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'

もし、shadow が指定しれていない場合は is-always-shadow クラスが bind されます。

<div class="el-card__header" v-if="$slots.header || header">
  <slot name="header">{{ header }}</slot>
</div>

次の div は el-card__header が固定で指定されています。
$slots.header、または prop の header が指定されている場所は {{ header }} に指定された要素が入り表示されます。

<div class="el-card__body" :style="bodyStyle">
  <slot></slot>
</div>

次の div は el-card__body が固定で指定されています。 また、style 要素に prop で指定する bodyStyle が bind されています。
bodyStyle は CSS スタイルを渡すことができます。

例){color: 'red', 'background-color': 'gray'}

<el-card :body-style="{color: 'red', 'background-color': 'gray'}">

このように body 部分のスタイルに適用されます。

image.png

<slot></slot><el-card></el-card> の中に記述したものがそのまま入ります。

今まででてきた要素を組み合わせてみました。

image.png

下記がソースになります。

<el-card class="box-card">
   <div slot="header"> <!-- ヘッダー -->
    <span>サンプルですよ</span> <!-- ヘッダーの内容1 -->
    <el-button style="float: right;">スロット=header に ボタン</el-button> <!-- ヘッダーの内容2 -->
  </div>
   <!-- ボディーのスロット -->
  ここが スロット です。
  ボタンをおいてみた
  <div>
    <el-button type="danger"> スロットにボタン </el-button>
   </div>
  <div>
     テーブルをおいてみた
    <table border>
       <tr>
         <th>ID</th><th>NAME</th><th>AGE</th>
       </tr>
       <tr>
         <td>1</td><td>ぺけぺけ</td><td>55</td>
       </tr>
       <tr>
         <td>2</td><td>ぷけぷけ</td><td>48</td>
       </tr>
     </table>
  </div>
</el-card>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Element から学ぶ Vue.js の component の作り方 その3 (card)

Element-ui とは

第3回になり、今まではあまりにも説明不足だったと反省しました。
Elemnt-ui は Vue.js のコンポーネントライブラリです。
Vue.js で作成されているため、インポートすることで様々なコンポーネントを利用可能になるます。
CSS フレームワークとも捉えることができ、デザイン済みのコンポーネントを簡単に利用することが可能です。
CSSフレームワークなので、ある程度整形されたコンポーネントに変更を加えて、自身のプロダクト色に染めることも可能です。

今回は Elemnt 2.52 がベースになってます。
公式ページ
https://element.eleme.io/#/en-US\

今回は card コンポーネントを解析していきたいと思います。
image.png

ソースの構成は

 index.js
  src
   |- main.vue

となっていいます。

main.vue

main.vue
<template>
  <div class="el-card" :class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'">
    <div class="el-card__header" v-if="$slots.header || header">
      <slot name="header">{{ header }}</slot>
    </div>
    <div class="el-card__body" :style="bodyStyle">
      <slot></slot>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'ElCard',
    props: {
      header: {},
      bodyStyle: {},
      shadow: {
        type: String
      }
    }
  };
</script>

main.vue を見ると、難しいことはしてなさそうです。

  <div class="el-card" :class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'">

上記では class を指定しています。

class="el-card" は固定で指定されます。
shadow は 指定されている場合は is-指定した値 が class になります。
指定されて表示が切り替わるのは always / hover / never のいずれかです。

image.png

:class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'

もし、shadow が指定されていない場合は is-always-shadow クラスが bind されます。

<div class="el-card__header" v-if="$slots.header || header">
  <slot name="header">{{ header }}</slot>
</div>

次の div は el-card__header が固定で指定されています。
$slots.header、または prop の header が指定されている場所は {{ header }} に指定された要素が入り表示されます。

<div class="el-card__body" :style="bodyStyle">
  <slot></slot>
</div>

次の div は el-card__body が固定で指定されています。 また、style 要素に prop で指定する bodyStyle が bind されています。
bodyStyle は CSS スタイルを渡すことができます。

例){color: 'red', 'background-color': 'gray'}

<el-card :body-style="{color: 'red', 'background-color': 'gray'}">

このように body 部分のスタイルに適用されます。

image.png

<slot></slot><el-card></el-card> の中に記述したものがそのまま入ります。

今まででてきた要素を組み合わせてみました。

image.png

下記がソースになります。

<el-card class="box-card">
   <div slot="header"> <!-- ヘッダー -->
    <span>サンプルですよ</span> <!-- ヘッダーの内容1 -->
    <el-button style="float: right;">スロット=header に ボタン</el-button> <!-- ヘッダーの内容2 -->
  </div>
   <!-- ボディのスロット -->
  ここが スロット です。
  ボタンをおいてみた
  <div>
    <el-button type="danger"> スロットにボタン </el-button>
   </div>
  <div>
     テーブルをおいてみた
    <table border>
       <tr>
         <th>ID</th><th>NAME</th><th>AGE</th>
       </tr>
       <tr>
         <td>1</td><td>ぺけぺけ</td><td>55</td>
       </tr>
       <tr>
         <td>2</td><td>ぷけぷけ</td><td>48</td>
       </tr>
     </table>
  </div>
</el-card>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript オブジェクトの参照 注意点

JavaScript 配列 参照の注意点

 配列の注意点の備忘録

  • Objectとは

    プリミティブ型(数、文字列、真偽値)以外のものを指す。
    => 配列はObject型

  1. プリミティブ型とObject型の違い

    プリミティブ型は値そのものが代入される。

    例)
    const number = 2  // 2そのものがnumberに入る
    

    Object型はその値が入っている参照(エリア)が代入される。

    例)
    const numbers = [1, 2]    // [1,2]の位置情報がnumbersに入る
    const numbers2 = numbers  // numbers2 : [1, 2]
    numbers[0] = 2            // numbers: [2, 2], numbers2: [2, 2]
    

    元の配列の値を操作すると、numbers2も同じところを指しているため、出力結果が同じ[2,2]になる。

  2. 対処方法

    別の参照を持たせれば、変更の影響は受けない。

    const numbers = [1, 2]
    const numbers2 = numbers.slice()  // コピーを作成
    numbers[0] = 2                    // numbers : [2, 2] , numbers2 : [1, 2]のまま
    

  numbers numbers2の参照が異なるため、numbers2は変更されない。

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

Javascript:<textarea>の文量に応じた自動高さ変更

Google翻訳のテキストエリアの様に高さがコンテンツの文量に応じて変化する<textarea>を実装します.

HTML
<textarea id="realArea" name="real" rows="15" cols="40"></textarea>

<p id="dummyArea"></p>

ダミーの<p>を用意します.

CSS
textarea#realArea {
    width: 50vw;
    height: 250px;
    font-size: large;
    font-family: inherit;
}

p#dummyArea {
    margin: 0;
    padding: 0;
    width: 50vw;
    text-align: left;
    font-size: large;
    font-family: inherit;
    visibility: hidden;
}

cssの設定がとても大事です.
- 幅をビューポート単位で本体とダミーが同じになる様にする.
- 同様にfont-sizeも本体とダミーが同じになる様にする.

JavaScript
const realArea = document.getElementById("realArea");
const dummyArea = document.getElementById("dummyArea");

const defaultHeight = 250; // CSSで決めたデフォルトの値と一致させる

window.onload = function() {
    realArea.addEventListener("input", function(evt) {
        dummyArea.innerHTML = evt.target.value.replace(/\n/g, "<br>") + "<br>";
        const newHeight = dummyArea.offsetHeight;
        if (newHeight > defaultHeight) {
            evt.target.style.height = newHeight + "px";
        } else {
            evt.target.style.height = defaultHeight + "px";
        }
    }, false);
}

こんなに短いコードでも驚くほどいい感じに動きます.

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

拝啓 Google様、JavaScriptとJSONで動的に変化するページをインデックスしてください

今回のお悩み

「Google様に、Webページ「A.html」をインデックスしてもらいたい」

そう、Webサイトを作るすべての人たちの願いだよね。

ーー

さて、今回インデックスしてもらいたいWebページ「A.html」はURLパラメータに応じて内容が変化するページである。
JavaScriptにより、URLパラメータに対応するデータを「data.json」から取得、レンダリングするというものだ(URLパラメータは「A.html?id=XX」と記述し、今回はid=1〜3まで存在するという設定)。

インデックスにはパラメータ別で登録したい。
つまり、id=1〜3の3ページ分を別々に登録する。

また、ページのタイトルやディスクリプションについても、パラメータごとに設定したものを検索結果に表示させたい。

最近のクローラーはJavaScriptでの処理も認識するとのことだが、果たして結果はー。

A.html
<!DOCTYPE HTML>
<html>
    <head>
        <meta name="description" content="仮のディスクリプション"/>
        <title>仮のタイトル</title>
        <script src="jquery.js"></script>
        <script src="script.js"></script>
    </head>
    <body>
        <div id="main"></div>
    </body>
</html>

↑jQueryを使うので要読込。

data.json
[
    {
        "id": 1,
        "title": "ページ1",
        "description": "これはページ1のディスクリプションです",
        "contents": "これはページ1の本文です"
    },
    {
        "id": 2,
        "title": "ページ2",
        "description": "これはページ2のディスクリプションです",
        "contents": "これはページ2の本文です"
    },
    {
        "id": 3,
        "title": "ページ3",
        "description": "これはページ3のディスクリプションです",
        "contents": "これはページ3の本文です"
    }
]
script.js
$(window).on("load", function() {
    //URLパラメータ(id)の値を取得
    var id = myGetQuery("id");
    //data.jsonのidとURLパラメータのidとを突合
    $.getJSON("data.json", function(data) {
        for (var i in data) {
            if (data[i].id === id) {
                //Title変更
                document.title = data[i].title;
                //Description変更
                $("meta[name='description']").attr("content", data[i].description);
                //本文追加
                $("#main").append("<p>" + data[i].contents + "</p>");
            }
        }
    )}
)}
//URLパラメータの抽出
function myGetQuery(myKeyWord) {
    myKeyWord = "&" + myKeyWord + "=";
    myValue = null;
    myStr = location.search;
    myLen = myStr.length;
    myStr = "&" + myStr.substring(1, myLen) + "&";
    myOfst = myStr.indexOf(myKeyWord);
    if (myOfst != -1) {
        myStart = myOfst + myKeyWord.length;
        myEnd = myStr.indexOf("&", myStart);
        myValue = myStr.substring(myStart, myEnd);
        myValue = decodeURIComponent(myValue);
    }
    return myValue;
}

↑一応言っておくが、何か特別クローラー対策をしているわけではない。

解決方法

サイトマップを用意する

サイトマップについては下記を参照
サイトマップについて - Search Console ヘルプ

※下記はXML形式で記述したもの
locタグに記述するURLにはURLパラメータを含める。
つまり、今回用意するサイトマップには3ページ分を記述する。

sitemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<url>
    <loc>https://www.sample/A.html?id=1</loc>
    <priority>1.0</priority>
</url>
<url>
    <loc>https://www.sample/A.html?id=2</loc>
    <priority>0.8</priority>
</url>
<url>
    <loc>https://www.sample/A.html?id=3</loc>
    <priority>0.5</priority>
</url>
</urlset>
</xml>

↑priorityは適当。

サイトマップを送信する

サイトマップが用意できたら、Google Search ConsoleでGoogle様に送信する(せっかちなヤツだと思われたくなければ、クローラーの巡回を待っても構わない)。

どうかインデックスされますように。

結果

無事、ページタイトル、ディスクリプションともにインデックスされ、検索結果に表示されるようになった。

斯くして、願いは叶えられた。
ありがとう、Google様。

めでたしめでたし。

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

brewでインストールしたyarnのバージョンを変更する

brewでインストールしているyarn(他のパッケージでも手順は基本的に同じです)のバージョンが新しすぎたため、古バージョンに戻す方法で詰まってしまった。この手の手順はすでにいくらでもあると思ったのだけれど、そもそも説明がまちがっていたり、homebrew/versionsがdeprecatedされる前の手順であったので新しく書いた。

前提

  • ローカルのMac環境において
  • yarn 1.13.0 をインストール済みだけど
  • yarn 1.12.3 に変更したい

やること

0. はじめに

前提:npmでyarnをインストールしている場合はアンインストールする。という手順が散見されるが、これは適当ではない。公式のドキュメントによると以下のように書かれているが、これはDebian、UbuntuおよびCentOSのような一般的なLinuxのディストリビューションでのお話。

注意: npm から Yarn をインストールすることは一般的にはお勧めしません。 Node ベースのパッケージマネージャで Yarn をインストールする場合は、パッケージは署名されておらず、整合性のチェックはベーシックな SHA1 ハッシュのみで行われており、システム全体にまたがるアプリケーションをインストールする場合にはセキュリティリスクとなります。

出典:https://yarnpkg.com/ja/docs/install#alternatives-stable

1. どこのyarnが呼ばれているか確認する

自分の環境ではanyenvを使っているのでこちらは問題なさそう。

$ which yarn
/Users/himatani/.anyenv/envs/ndenv/shims/yarn

2. brewのバージョンを確認する

which {package} コマンドによって意図してない結果が返ってきた場合は、そもそもyarnをインストール済みであることを確認する。インストールしていなければ brew install yarn で入れる(最新のバージョンでインストールされる)。

$ brew list yarn
yarn

自分の環境では最新(2019/02/18 現在)の 1.13.0 が入っていた。ちなみに、 brew list --versions コマンドでパッケージ名を指定すると、インストール済みのすべてのバージョンが確認できる。

$ yarn -v
1.13.0

$ brew list --versions yarn
yarn 1.13.0

3. 旧バージョンが提供されていないか探す

Homebrewの公式から古いバージョンが提供されていないか探すために、以前であれば以下のようにして検索できたがすでにdeprecatedとなってしまっている。(2019/02/18 現在)

$ brew tap homebrew/versions
$ brew search versions/yarn

4. 旧バージョンのFormulaファイルを探す

仕方がないので、Homebrewの全コミットから自分が欲しいバージョンを探していく。まずは、Homebrewのリポジトリがあるディレクトリまで移動する。

$ cd /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula

git log {package}.rb などでgitのログを絞ってコメントを頼りに、欲しいバージョンのコミットを見つける。

$ git log yarn.rb

古いバージョンのFormulaファイルからインストールするために、コミットのハッシュ値を抜き出す。ちなみに yarn 1.12.3 のコミットのハッシュ値は 66645f26c48fb949dfc8784dfc1b4c7ea3ead1b1 であった。

5. 旧バージョンのFormulaファイルでインストールする

こちらのコマンド brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/{hash}/Formula/{package}.rb でコミットのハッシュ値とパッケージ名を指定してbrew installできる。先ほどの手順で見つけたコミットのハッシュ値を入れてインストールする。

(ちなみに、ここで該当のコミットのハッシュ値でcheckoutして、brew install {package} で旧バージョンのFormulaファイルからインストールするやり方もある。ただ自分は、インストール後にHEADに戻すのを忘れるのもあれなので、以下の手順の方が安全にインストールすればいいと思っている。)

$ brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/66645f26c48fb949dfc8784dfc1b4c7ea3ead1b1/Formula/yarn.rb

すでに最新のバージョンでインストールされているよ、と言われた場合は brew unlink {package} でunlinkしてあげる。

Error: yarn 1.13.0 is already installed
To install 1.12.3, first run brew unlink yarn

無事にインストールできた :beer:

$ brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/66645f26c48fb949dfc8784dfc1b4c7ea3ead1b1/Formula/yarn.rb

######################################################################## 100.0%
Warning: yarn 1.13.0 is available and more recent than version 1.12.3.
==> Downloading https://yarnpkg.com/downloads/1.12.3/yarn-v1.12.3.tar.gz
==> Downloading from https://github-production-release-asset-2e65be.s3.amazonaws.com/49970642/a4875000-e25c-11e8-88b4-45d26e231bb1?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F
######################################################################## 100.0%
?  /usr/local/Cellar/yarn/1.12.3: 14 files, 4.7MB, built in 11 seconds
Removing: /usr/local/Cellar/yarn/1.6.0_1... (14 files, 4.1MB)
Removing: /Users/himatani/Library/Caches/Homebrew/yarn--1.12.3.tar.gz... (1.1MB)

インストールできると自動でインストールしたバージョンでlinkしてくれる。

$ yarn -v
1.12.3

以前のバージョンもきちんと残っている。

$ brew list --versions yarn
yarn 1.13.0 1.12.3

ちなみに、brew switch {package} {version} で使用するバージョンの切り替えができる。

$ brew switch yarn 1.13.0

めでたしめでたし。

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

ド初心者が初めてのWebサービスを作ってみた

概要

普段は組み込み系ソフト開発の仕事をしているのですが、簡単なのでもいいからWebサービスをサクッと作れると
出来ること広がるかなぁと思って挑戦してみました。その過程で思ったこと、遠回りしたことの備忘録です。

作ったものと動機

Latitude and Longitude Unit Converter

緯度・経度を種々の単位で相互変換するサイトです。Webサービスというか、ただの便利計算サイトです。

こういう単位変換サイトは既に沢山あるのですが、よく使われる単位を網羅し、かつGoogle Mapで場所を表示できるサイトは意外となかったので作成してみました。(つまり半分自分の本業用です)

なお、入力値の範囲チェックなどが未だちゃんとしてません。今後修正予定です。

やってみようと思った時点でのスキル状態

  • 本業でPython/Linuxはある程度慣れている
  • Javascriptは1行も書いたことがない、変数宣言のやり方も知らない
  • WebアプリケーションフレームワークとWebサーバの違いが分かっていない
  • サーバ管理も経験がない
  • HTML/CSSは見たことは流石にあるが自分で書いたことはない

困ったこと

とにかく全体像を体系立てて把握できていないことが辛かったです。
QiitaにWeb関連の記事は多いですが、キーワードだけでもNode.js, Javascript, Django, Ruby on Rails, HTML, CSS, Bootstrap, Typescript, Apache, nginx, Laravel, jQuery, Vue.js, React, Gunicorn etc. etc.といろんな情報が溢れていて何がなんだか、という感じでした。

  • AngularとかVue.jsとか沢山Qiita記事あるけどWebサイト作るのに必須なの?
  • Vue.jsとRuby on Railsは何が違うの?

みたいな感じで見当違いな困惑を持っていました。

最初に始めたこと

最初はDjango GirlsのTutorialから始めました。
明確な意図があったわけではないですが、Pythonは本業で使っていたのでとりあえずPythonの「Webっぽいもの」をやってみようと思った訳です。「左手を伸ばしたら壁があったのでとりあえず壁伝いに歩いてみた」という感じです。

Django GirlsのTutorialはかなり親切で、"How the Internet works"レベルからだったので色々勉強にはなりました。
また、templateを使って実際にHTMLを書いて表示してみて、手触りは得られた気がします。

今になって思えば、今回作ったものはDjangoなんて必要ありませんでした。(DjangoでModelを定義するとそれがDB設計に繋がるようですが、今回のサイトはDBなんて使っていない)
しかしながら、このおかげで世の中の多数のWebサービスが本質的にはDBの更新またはDBの内容表示に過ぎないことを理解できました。
(アタリマエのことをと言われそうですうが、その程度も私にとっては発見でした)

作るネタを考える

この時点ではまだ緯度経度の変換サイトを作るつもりはありませんでした。もっと素晴らしい物を作る気でいました笑。しかし調べれば調べる程、一発目は学習コストを最小限にしないとキリがないという気分になってきました。
そこで「本業であったら便利だな」と思っている緯度経度変換に特化して作ろう、と決めました。

Javascriptで緯度経度変換の実装

作ると決まればあとはひたすら実装なわけですが、この緯度経度の変換の実装をJavascriptでやるべきなのか、Django側でPythonでやるべきなのかがわかっていませんでした。
この時「いちいち変換ボタンを押さずに動的に変換したい」と思っていて、どうもそれをやるにはJavascriptで実装しないといけないらしいと分かったのでJavascriptで実装しました。

Typescriptで書くとJavacriptよりも慣れた形で書けそうだ、ということは分かっていたのですが、
Javascriptを一度も書いたことがない状態ならまずはJavascriptの基本は知っておくべきだろうと思って
素のJavascriptで書くことにしました。その過程でJSテストフレームワークのJestも使ってみました。

Google Map APIで地図を表示する

Googleの公式説明が充実しているのでそんなに手こずりませんでした。
しかしながら、手違いで無料枠を超えたら怖いのでAPIの各種restrictionsやquota(回数制限)の設定は緊張しました。

VPSを借りる

Djangoで作ったアプリをlocalhostで動かすことは慣れてきたものの、
ではこれを一般に公開するにはどうしたら良いのか、と調べてみて
どうやらVPS(Virtual Private Server)を借りてそこでnginxを動かせばればよいと理解しました。
(今思うと、先にそっちをやって"Hello worldでいいから先に公開して全体像を掴んでおくべきだった"と思います)

無難にさくらインターネット様かConoHa様かで迷いましたが、美雲このはちゃん可愛いなーと思ってConoHa様で月額900円のプランをお借りしました。OSは本業で使い慣れているUbuntuにしました。

nginxとuWSGIでハマる

VPS上でDjangoで作ったWebアプリとnginxを接続するためにuWSGIというものを使う必要があると理解し、
多少苦戦しました。設定ファイル登場し過ぎだよ。エラーメッセージを沢山読んで乗り切った。

振り返り、感想

  • 私の場合、Djangoを勉強してからVPSを借りてnginxを入れて公開という順序だったが逆だった。先にVPSを借りてHello worldだけでいいから公開すべきだった。
  • DjangoじゃなくてFlaskでも十分だった。というかWebアプリケーションフレームワーク使う必要なかった。勉強する過程で気づきもあったので完全に無駄とまでは言えないが。
  • やることが広く浅くなので、横着せずにやったことや打ったコマンドはMarkdownでまとめておく。時間が空くと忘れる。
  • Javascriptが標準ではCやPythonでいう#include, importの類を使えないことに焦った。var変数のスコープにも衝撃を受けた。
  • すいてて長時間滞在を許してくれる喫茶店の確保は重要。コーヒーだけじゃなく食事も注文すること。コーヒーは3時間に1回はおかわり注文すること。

未だに分かっていないこと

  • AngularやVueって、どういうことをしたくなったら必要になるの?
  • 現在はnginx + uWSGI + Djangoで動かしているが、Gunicornて必要なの?

最後に

思ったことをつらつらと書いておりますが、間違っている理解があれば遠慮なくご指摘いただけますと幸いです。
よろしくお願い致します。

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

FC2ブログの爆速テンプレートを解析してみた

2019年2月 爆速テンプレートが公開された

先日、FC2ブログにおいて「爆速テンプレート」というものがリリースされました。

これはその名の通りタフなニューメキシコの荒野でもブログが爆速で見れるテンプレートで、爆速である代わりにカスタマイズの自由度は通常のテンプレートよりもかなり低い、というもの。

その爆速具合を実際に体感できるようにサンプル用の爆速テンプレートブログが公開されていますので、具体的にどういうものか知りたい方は下記のリンク先で体感してください。

爆速テンプレート サンプルブログ

この記事はその爆速さに興味をもってその中身を解析してみた結果をここにまとめておいたものです。

ただし飽くまで外部の人間が簡単に解析してみた結果であるため間違いや事実誤認が予想されます、気が付いた方はコメントにてご指摘ください。

なお ServiceWorker や CacheAPI に関する説明はここではするつもりはありませんが、仮にそれらに関する知識がなくても理解できるように書いたつもりです。

それらについての詳細を知りたい場合は下記のリンク先を参照してください。

Service workeの使用例|MDN
Cache -Web API|MDN

爆速である要因

リクエストが少ない

最も影響が大きいのがサービスの設計自体が最適化されていることで、具体的には必要な外部ファイルがそもそも少ない。

通常のブログサービスと比較しても1/3から1/5程度のファイル量しか読み込む必要がなく、比較対象をよくあるゴテゴテしたまとめブログにした場合は1/10以下である。

Instant Click

ページを解析してみたところ「Instant Click」というJSライブラリを利用している、これは dev.to などのように、「リンクをマウスオーバーした時点でリンク先を裏で読み込んで、BODYの中身を差し替える」というページ先読み+pjaxを簡単に実現してくれるライブラリです。

これはリンクをクリックする際に、マウスオーバーしてから実際にクリックが確定されるまで200~300ms程度の時間がかかることを利用してページを先読みするもので、想像以上に体感速度が早く感じられるようになります。

ちなみに記事をスクロールした時の追加読み込みもこの Instant Click で実装しているようです。

InstantClick

データの先読み

ServiceWorker の install 時に9記事分のページのデータが先読みされています。

つまりWEBページにアクセスした時にページの読み込みをしている「裏」で、ServiceWorker が9記事分のデータを取得してキャッシュに追加しているということです。

DNSプリフェッチ

これはページ読み込み時に外部ファイルにアクセスするより先にDNSの名前解決をしてしまうもので、100~200ms程度の時間がこれにより短縮できるとされています。

DNSプリフェッチの制御|MDN

キャッシュ更新について(推測)

ここで気になるのが「キャッシュ更新のロジック」でブログというサービスの都合上、記事内容が更新されたり削除されたりすることは頻繁にありえる。

その場合キャッシュを保持していると、いつまでも更新前の記事を表示することになってしまうので、なんらかのタイミングでキャッシュを更新する必要があるはず。

ServiceWorker はおそらく動的に生成されており、それにより先読みするページの変更とキャッシュの更新を行っているようだが、具体的にどういうタイミングで更新をかけているのかは不明。

ServiceWorker が install されたタイミングで両者の更新を行っているようなので、記事が追加/更新されたタイミングで ServiceWorker が変更されて install -> キャッシュ更新というロジックで更新されるように推測される。

上記が正しければ、要するに ServiceWorker の更新とキャッシュの更新は一体化しており、キャッシュのみを更新するロジックはないということなる。

まとめ

爆速の主要因は「リクエスト数の少なさ」と「InstantClick」であるように見受けられる。

ちなみにキャッシュは先読みで最初の9記事だけをもっており、それ以外の記事についてはキャッシュを利用せずに純粋に InstantClick でのみ体感速度を向上させているようで、個人的には後続の記事をキャッシュしていないのは意外だった。

またこの記事ではフロント側に寄った説明をしているが、当然バックエンド側やサービスの設計も最適化されてるので、そもそもこのブログなら”素”でも速い。

実際にカテゴリ一覧や月別記事一覧から記事を閲覧するとキャッシュにない記事のはずだが、それでも十分高速さが体感できるので、フロント側の実装が通常のブログのものと同様であったとしても充分に高速であったはずだと思われる。

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

FC2ブログの爆速テンプレートはなぜ早いのか?

2019年2月 爆速テンプレートが公開された

先日、FC2ブログにおいて「爆速テンプレート」というものがリリースされました。

これはその名の通りタフなニューメキシコの荒野でもブログが爆速で見れるテンプレートで、爆速である代わりにカスタマイズの自由度は通常のテンプレートよりもかなり低い、というもの。

その爆速具合を実際に体感できるようにサンプル用の爆速テンプレートブログが公開されていますので、具体的にどういうものか知りたい方は下記のリンク先で体感してください。

爆速テンプレート サンプルブログ

この記事はその爆速さに興味をもってその中身を解析してみた結果をここにまとめておいたものです。

ただし飽くまで外部の人間が簡単に解析してみた結果であるため間違いや事実誤認が予想されます、気が付いた方はコメントにてご指摘ください。

なお ServiceWorker や CacheAPI に関する説明はここではするつもりはありませんが、仮にそれらに関する知識がなくても理解できるように書いたつもりです。

それらについての詳細を知りたい場合は下記のリンク先を参照してください。

Service workeの使用例|MDN
Cache -Web API|MDN

爆速である要因

リクエストが少ない

最も影響が大きいのがサービスの設計自体が最適化されていることで、具体的には必要な外部ファイルがそもそも少ない。

通常のブログサービスと比較しても1/3から1/5程度のファイル量しか読み込む必要がなく、比較対象をよくあるゴテゴテしたまとめブログにした場合は1/10以下である。

Instant Click

ページを解析してみたところ「Instant Click」というJSライブラリを利用している、これは dev.to などのように、「リンクをマウスオーバーした時点でリンク先を裏で読み込んで、BODYの中身を差し替える」というページ先読み+pjaxを簡単に実現してくれるライブラリです。

これはリンクをクリックする際に、マウスオーバーしてから実際にクリックが確定されるまで200~300ms程度の時間がかかることを利用してページを先読みするもので、想像以上に体感速度が早く感じられるようになります。

ちなみに記事をスクロールした時の追加読み込みもこの Instant Click で実装しているようです。

InstantClick

データの先読み

ServiceWorker の install 時に9記事分のページのデータが先読みされています。

つまりWEBページにアクセスした時にページの読み込みをしている「裏」で、ServiceWorker が9記事分のデータを取得してキャッシュに追加しているということです。

DNSプリフェッチ

これはページ読み込み時に外部ファイルにアクセスするより先にDNSの名前解決をしてしまうもので、100~200ms程度の時間がこれにより短縮できるとされています。

DNSプリフェッチの制御|MDN

キャッシュ更新について(推測)

ここで気になるのが「キャッシュ更新のロジック」でブログというサービスの都合上、記事内容が更新されたり削除されたりすることは頻繁にありえる。

その場合キャッシュを保持していると、いつまでも更新前の記事を表示することになってしまうので、なんらかのタイミングでキャッシュを更新する必要があるはず。

ServiceWorker はおそらく動的に生成されており、それにより先読みするページの変更とキャッシュの更新を行っているようだが、具体的にどういうタイミングで更新をかけているのかは不明。

ServiceWorker が install されたタイミングで両者の更新を行っているようなので、記事が追加/更新されたタイミングで ServiceWorker が変更されて install -> キャッシュ更新というロジックで更新されるように推測される。

上記が正しければ、要するに ServiceWorker の更新とキャッシュの更新は一体化しており、キャッシュのみを更新するロジックはないということなる。

まとめ

爆速の主要因は「リクエスト数の少なさ」と「InstantClick」であるように見受けられる。

ちなみにキャッシュは先読みで最初の9記事だけをもっており、それ以外の記事についてはキャッシュを利用せずに純粋に InstantClick でのみ体感速度を向上させているようで、個人的には後続の記事をキャッシュしていないのは意外だった。

またこの記事ではフロント側に寄った説明をしているが、当然バックエンド側やサービスの設計も最適化されてるので、そもそもこのブログなら”素”でも速い。

実際にカテゴリ一覧や月別記事一覧から記事を閲覧するとキャッシュにない記事のはずだが、それでも十分高速さが体感できるので、フロント側の実装が通常のブログのものと同様であったとしても充分に高速であったはずだと思われる。

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

ES2018の正規表現の新機能を試してみた

参考

↑の内容を元に試しただけという内容ですので、正しい情報が欲しい方は参考ページを見て、本記事は見なくても良いです。
自分の備忘録として投稿させていただきました。
間違っている箇所などあれば、コメントなどよろしくお願いいたします :bow:


tl;dr

以下の機能が追加されるようなので試します

  • s (dotAll) flag for regular expressions
  • RegExp named capture groups
  • RegExp Lookbehind Assertions
  • RegExp Unicode Property Escapes

http://kangax.github.io/compat-table/es2016plus/

ちなみにすでにchromeでは動くので、babelなどを使わずそのまま実行していきます。

※2019/2/17現在

image.png

s (dotAll) flag for regular expressions

オプション無しのデフォルトでは、. (ドット)は改行文字にはマッチしないのですが、s オプションをつけることで改行にもマッチします

// sオプション無し -> false
/foo.bar/.test(`foo
bar`)

// sオプション有り -> true
/foo.bar/s.test(`foo
bar`)

Screen Shot 2019-02-17 at 23.18.56.png

RegExp named capture groups

マッチした文字列を連想配列みたいな感じで取れる。

const { year, month, day } = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/.exec('2020-03-04').groups
console.log(year, month, day) // 2020 03 04

↓のように groupsから値を受け取れます。

image.png

\kで使いまわすこともできる!

const re = /(?<dup>ABC)x+\k<dup>/.test('ABCxxxABC'); // true

image.png

replaceでも使うことができる!

console.log('War & Peace'.replace(/(War) & (Peace)/, '$2 & $1'));  // Peace & War 
console.log('War & Peace'.replace(/(?<War>War) & (?<Peace>Peace)/, '$<Peace> & $<War>'));  // Peace & War

// 関数だとこうなる
const result = 'War & Peace'.replace(/(?<War>War) & (?<Peace>Peace)/, function(match, group1, group2, offset, string) {
    return group2 + ' & ' + group1;
});
console.log(result);    // Peace & War

image.png

RegExp Lookbehind Assertions

今まで先読みしかできなかったのですが、後読みの機能が追加されたようです。

console.log(/(?<=a)b/.test('ab')) // true
console.log(/(?<!a)b/.test('cb')) // true

image.png

以下のページも参考

情報ありがとうございます:bow:

RegExp Unicode Property Escapes

以下のページのreadmeを参考にしつついくつかサンプルを試してみました

Number以外にもありますが、一番手軽に試せたのでNumberで確認しました。

// 半角の1
console.log(/\p{Number}/u.test(1)); // true

// 全角の1
console.log(/\p{Number}/u.test('1')); // true

// ローマ数字などの10進以外の記号を含む、Unicodeの任意の数字記号と一致
console.log(/^\p{Number}+$/u.test('²³¹¼½¾????????????????㉛㉜㉝ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿ')); // true

\p\Pとすると否定になるようです。

image.png


最後まで読んでいただきありがとうございましたm(_ _)m

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

contenteditable でキャレットを先頭と末尾に移動させる

contenteditable でキャレットを先頭と最後に移動させる方法

キャレットを先頭に移動

const target = document.getElementById('example')
const node = target.childNodes[0]
const editorRange = document.createRange()
const editorSel = window.getSelection()
editorRange.setStart(node, 0)
editorRange.collapse(true)
editorSel.removeAllRanges()
editorSel.addRange(editorRange)

キャレットを末尾に移動

const target = document.getElementById('example')
const p = target.childNodes[target.childNodes.length - 1]
const node = p.childNodes[p.childNodes.length - 1]
const editorRange = document.createRange()
const editorSel = window.getSelection()
editorRange.setStart(node, node.childNodes.length ? node.childNodes.length : node.length)
editorRange.collapse(true)
editorSel.removeAllRanges()
editorSel.addRange(editorRange)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript で対象 node すべてを選択する2つの方法

対象の node をすべて選択するには以下の2つのやり方がある。

<div id="example">
  <p>hoge</p>
  <p>hoge</p>
</div>
const target = document.getElementById('example')
const range = document.createRange()
const sel = window.getSelection()
range.selectNodeContents(target)
sel.removeAllRanges()
sel.addRange(range)
const target = document.getElementById('example')
const range = document.createRange()
const sel = window.getSelection()
range.setStart(target.childNodes[0], 0)
const endNode = target.childNodes[target.childNodes.length - 1]
range.setEnd(endNode, endNode.childNodes.length)
sel.removeAllRanges()
sel.addRange(range)

それぞれ同じように対象の node をすべて選択できるが、range オブジェクトが微妙に異なる。前者の selectNodeContents で対象 node を入れた場合は startContainer / endContainer / commonAncestorContainer が自身の node になる。つまり対象 node 自身を含んで選択していることになる。

後者の setStartsetEnd で指定している場合は、commonAncestorContainer は自身の node だが、startContainer / endContainer はそれぞれ setStartsetEnd で指定した node になる。これはユーザが手動で文字列を選択したときと同じような range オブジェクトになる。

どちらが良いというよりかは、それぞれ場合によって使い分ける。

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

angularでのバリデーション実装方法

はじめに

Angularを使用していて、バリデーションをするときに手こずったので、自分用と同じ問題で困っている人のために実装方法を記録しておきたいと思います。

実装例

<form #loginForm="ngForm" novalidate>
  <label for="mail-address">メールアドレス</label>
  <input id="mail-address" type="email" name="mail"
    [(ngModel)]="login.mail" #mail="ngModel" required email/>
  <div *ngIf="mail.errors && (mail.dirty || mail.touched)"
    class="alert alert-danger">
    <div [hidden]="!mail.errors.required">
      入力してください
    </div>
  </div>
</form>
import { Component, OnInit } from '@angular/core';

@Component({
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss'],
})
export class LoginComponent implements OnInit {
  login = {
    mail: '',
    password: ''
  };
  constructor() {}

  ngOnInit() {
  }
}

注意点

以下処理を入れないと、ページ表示時にもバリデーションが働いてしまうので注意する必要がある。
html
*ngIf="mail.errors && (mail.dirty || mail.touched)"

参考

https://qiita.com/mixplace/items/047f6edfe99d28af1dab

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