- 投稿日:2019-02-18T23:54:02+09:00
[Angular] テキストエリアとボタンを使ったコンポーネントのテストを書く
環境
- Angular CLI: 7.3.1
たぶん他のバージョンでも動く
準備
事前準備
テキストエリアやボタンを使うのにFormsModule必要です。
app.modules.tsimport { FormsModule } from '@angular/forms'; // ... imports: [ FormsModule, // ...app.component.spec.tsbeforeEach(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.tsimport { 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.tsbeforeEach(async(() => { TestBed.configureTestingModule({ // ... providers: [ { provide: ComponentFixtureAutoDetect, useValue: true } // ...CompoentFixtureAutoDetectはある程度コンポーネント変更検知を自動検知してくれる。
何度もdetectChanges()書かなくて済む
まぁ、DOMイベントは自動検知してくれないけど(以下のサンプルでは必須)。本題
inputイベントが発行されたことをテストする
app.compoent.spec.tsit('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.tsit('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.tsit('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('なんか書いた'); });参考記事
- Angular 日本語ドキュメンテーション - テスト
- HTMLElement - MDN - Mozilla
- HTMLTextAreaElement - MDN - Mozilla
- HTMLButtonElement - MDN - Mozilla
- KeyboardEvent - MDN - Mozilla
- JavaScriptでキーボードの入力値を別の値しようとしたらめちゃめちゃ苦しんだ話
間違いなどありましたら気兼ねなくご指摘ください
- 投稿日:2019-02-18T22:30:36+09:00
IEX APIで暗号通貨のレート情報を一覧表示
IEX APIに暗号通貨の情報を取得するものがあったので、それを使用してレート情報を一覧形式で表示します(10秒ごとに自動更新)。
実行イメージ
実際に動いている様子は下記記事で見れます。
【プログラミング】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
- 投稿日:2019-02-18T22:05:17+09:00
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はこちらから
- 投稿日:2019-02-18T18:21:37+09:00
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 を使います。
ライブラリの取得
- Bowerやnpmといったツールを使う → GitHub の README
- 必要なコンポーネントのみダウンロード → build tool
- CDN → https://cdnjs.com/libraries/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 にあるように、メンテナンスされていません
- 投稿日:2019-02-18T18:15:49+09:00
ブレークポイントを貼らずにアニメーション処理などの途中で画面を一時停止させる方法
概要
ビルドされたファイルが難読化されてたりすると該当部分がよくわからないことがあり、ブレークポイントを貼るのに時間がかかったりする。
そんなときはアニメーション処理のコードの途中に
debugger;を書けばGoogle Chromeのデベロッパーツールが画面を停止させてくれる。アニメーション中の細かい位置などを確認したいときにおすすめ。
出典
コードをステップ実行する方法 | Tools for Web Developers | Google Developers
onClick が呼び出されるたびに、f という関数が呼び出され、スクリプトは強制的に debugger キーワードで一時停止されます。
- 投稿日:2019-02-18T16:38:42+09:00
指定した何れかの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; } })();
- 投稿日:2019-02-18T16:03:15+09:00
ブラウザを閉じるまで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.jsvar 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.jsvar 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.jswindow.addEventListener('load', addSessionStorage);
- 投稿日:2019-02-18T15:27:05+09:00
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でした^^
- 投稿日:2019-02-18T13:28:06+09:00
Vueでダイアログを動的に生成してマウントするサンプル作ってみた
課題
Vueでサービスを作っててダイアログやモーダルを表示したいときに、公式だとdataにフラグを持たせてClickイベントでtrue/falseを切り替えるようにしていた。ただ、これだと画面内で表示したいダイアログが増えるほどにdataのフラグも増えるし、テンプレートの中に各フラグと紐づいたdialogタグを量産することになって、めちゃくちゃ見にくいし何より格好悪い。
なので、showDialog()的な感じでメソッド呼び出しでダイアログを表示させたいやったこと
renoinn/vue-dialog-sample
というわけで、DialogHelper.showDialog()とすることでダイアログを表示させるサンプルを作ってみた。解説
キモになるのは以下の点
Dialog.vuemethods: { 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.jsimport 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.$root
やthis.$parent
を渡すこともできる。今回はダイアログだったけど、同じような感じでモーダルやトーストも実装できる。
参考URL
https://qiita.com/hako1912/items/8c0462203987f2cd15b1
https://kitak.hatenablog.jp/entry/2017/04/04/044829
https://github.com/paliari/v-slim-dialog
- 投稿日:2019-02-18T13:14:26+09:00
[##]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
- 投稿日:2019-02-18T13:14:26+09:00
[##]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
- 投稿日:2019-02-18T13:11:30+09:00
動的に追加した要素のイベントを拾う
動的に追加した要素のイベントが拾えませんと相談されたのでサンプルを書きました.
例えば「行追加」ボタンを押したらテーブルに行が追加される場合です.
最初に表示されている行の「イベント発火!」ボタンを押すと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.
- 投稿日:2019-02-18T12:59:15+09:00
[#6]Paper.jsを始めてみよう -ベクトルの扱いについて理解しよう 2-
こんにちは。yokuneruです。
前回の[#5]-ベクトルの扱いについて理解しよう1-に引き続き、Paper.jsの解説をしていきます。
今回は公式チュートリアルのVector Geometryを参考に解説していきます。
ベクトルの回転と角度の操作
ベクトルを回転させることはパスや形状を構築するための強力なツールです。例えば、あるベクトルから一定の角度を回転させたベクトルを定義することを可能にします。
[マウスベクトルの操作チュートリアル(後日公開)]ではベクトルの回転を利用して、移動したマウスの方向と平行にパスを作成するサンプルを紹介します。Paper.jsでは水平軸から時計回りに角度が計測され、180°で値が-180°に反転されます。角度を180°以上に設定することも可能です。
ベクトルの角度を変えるには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}
![]()
2つ目の方法はベクトルの角度に数値を足し合わせる方法です。
以下の例ではangleプロパティを用いて、現在の角度(45°)に90°足し合わせています。vector.angle += 90; console.log(vector.angle); // 135 console.log(vector); // {x: -100, y: 100}どちらの方法でも同じ結果を得ることができますが、ベクトルの長さは変わらないことに注意してください。
メソッドとプロパティの操作
四則演算や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>
上記のコードではこのような結果が得られ、元の赤線が描画されないことがわかります。一方、ベクトルの角度や長さを変更すると、ベクトルオブジェクトが直接編集されます。そのため、元のオブジェクトを変更したくない場合には以下のように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";
元の赤線が残された上で、新たに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>このような画面が表示されれば描画成功です。
ドラッグ&ドロップするとそれに応じた矢印が描画されます。
「ベクトルの扱いについて理解しよう 2」は以上です。
続きは[#7]-パスの扱いについて理解しよう-です。また、本連載の一覧ページもありますので、そちらもぜひご覧ください。
*Twitterもフォローして頂けると幸いです。
こちら→@yokuneru_gs
- 投稿日:2019-02-18T12:08:07+09:00
Jestで AWS SDKモジュールをモック化する方法(promise込み)
Jestでモック化できるケース・できないケース
普通にモック化できるケース
通常。関数やモジュールは、requireして使用していれば、モックオブジェクトを差し込むことができる。
module_a.jsmodule.exports = { featureA (paramA, ParamB) => { ... },module.spec.jsconst 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.jsconst 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.jsconst 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.jsconst 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.jsconst moduleA = require('module_a') test('exist cognito user', async () => { moduleA.cognitoidp.adminGetUser = jest.fn() .mockReturnValue({ promise: jest.fn() .mockResolvedValue({ UserAttributes: [ { attr: "value" } ] }) }) ...
- 投稿日:2019-02-18T11:40:24+09:00
クリックした次の要素をトグルする
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>
- 投稿日:2019-02-18T10:17:21+09:00
癖ある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.isNaNconsole.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; }
- 投稿日:2019-02-18T10:17:21+09:00
癖ある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.isDeepStrictEqualisNaN関数を使う
サンプル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.isNaNconsole.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)」と言うようです。
- 投稿日:2019-02-18T09:43:11+09:00
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 コンポーネントを解析していきたいと思います。
ソースの構成は
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
のいずれかです。
: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 部分のスタイルに適用されます。
<slot></slot>
は<el-card></el-card>
の中に記述したものがそのまま入ります。今まででてきた要素を組み合わせてみました。
下記がソースになります。
<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>
- 投稿日:2019-02-18T09:43:11+09:00
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 コンポーネントを解析していきたいと思います。
ソースの構成は
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
のいずれかです。
: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 部分のスタイルに適用されます。
<slot></slot>
は<el-card></el-card>
の中に記述したものがそのまま入ります。今まででてきた要素を組み合わせてみました。
下記がソースになります。
<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>
- 投稿日:2019-02-18T06:56:41+09:00
JavaScript オブジェクトの参照 注意点
JavaScript 配列 参照の注意点
配列の注意点の備忘録
Objectとは
プリミティブ型(数、文字列、真偽値)以外のものを指す。
=> 配列はObject型
プリミティブ型と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]になる。
対処方法
別の参照を持たせれば、変更の影響は受けない。
const numbers = [1, 2] const numbers2 = numbers.slice() // コピーを作成 numbers[0] = 2 // numbers : [2, 2] , numbers2 : [1, 2]のままnumbers numbers2の参照が異なるため、numbers2は変更されない。
- 投稿日:2019-02-18T03:45:18+09:00
Javascript:<textarea>の文量に応じた自動高さ変更
Google翻訳のテキストエリアの様に高さがコンテンツの文量に応じて変化する
<textarea>
を実装します.HTML<textarea id="realArea" name="real" rows="15" cols="40"></textarea> <p id="dummyArea"></p>ダミーの
<p>
を用意します.CSStextarea#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
も本体とダミーが同じになる様にする.JavaScriptconst 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); }こんなに短いコードでも驚くほどいい感じに動きます.
- 投稿日:2019-02-18T01:23:13+09:00
拝啓 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様。めでたしめでたし。
- 投稿日:2019-02-18T01:19:05+09:00
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/yarn2. 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.03. 旧バージョンが提供されていないか探す
Homebrewの公式から古いバージョンが提供されていないか探すために、以前であれば以下のようにして検索できたがすでにdeprecatedとなってしまっている。(2019/02/18 現在)
$ brew tap homebrew/versions $ brew search versions/yarn4. 旧バージョンの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 runbrew unlink yarn
無事にインストールできた
$ 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めでたしめでたし。
- 投稿日:2019-02-18T00:58:18+09:00
ド初心者が初めての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て必要なの?
最後に
思ったことをつらつらと書いておりますが、間違っている理解があれば遠慮なくご指摘いただけますと幸いです。
よろしくお願い致します。
- 投稿日:2019-02-18T00:55:26+09:00
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 で実装しているようです。
データの先読み
ServiceWorker の install 時に9記事分のページのデータが先読みされています。
つまりWEBページにアクセスした時にページの読み込みをしている「裏」で、ServiceWorker が9記事分のデータを取得してキャッシュに追加しているということです。
DNSプリフェッチ
これはページ読み込み時に外部ファイルにアクセスするより先にDNSの名前解決をしてしまうもので、100~200ms程度の時間がこれにより短縮できるとされています。
キャッシュ更新について(推測)
ここで気になるのが「キャッシュ更新のロジック」でブログというサービスの都合上、記事内容が更新されたり削除されたりすることは頻繁にありえる。
その場合キャッシュを保持していると、いつまでも更新前の記事を表示することになってしまうので、なんらかのタイミングでキャッシュを更新する必要があるはず。
ServiceWorker はおそらく動的に生成されており、それにより先読みするページの変更とキャッシュの更新を行っているようだが、具体的にどういうタイミングで更新をかけているのかは不明。
ServiceWorker が install されたタイミングで両者の更新を行っているようなので、記事が追加/更新されたタイミングで ServiceWorker が変更されて install -> キャッシュ更新というロジックで更新されるように推測される。
上記が正しければ、要するに ServiceWorker の更新とキャッシュの更新は一体化しており、キャッシュのみを更新するロジックはないということなる。
まとめ
爆速の主要因は「リクエスト数の少なさ」と「InstantClick」であるように見受けられる。
ちなみにキャッシュは先読みで最初の9記事だけをもっており、それ以外の記事についてはキャッシュを利用せずに純粋に InstantClick でのみ体感速度を向上させているようで、個人的には後続の記事をキャッシュしていないのは意外だった。
またこの記事ではフロント側に寄った説明をしているが、当然バックエンド側やサービスの設計も最適化されてるので、そもそもこのブログなら”素”でも速い。
実際にカテゴリ一覧や月別記事一覧から記事を閲覧するとキャッシュにない記事のはずだが、それでも十分高速さが体感できるので、フロント側の実装が通常のブログのものと同様であったとしても充分に高速であったはずだと思われる。
- 投稿日:2019-02-18T00:55:26+09:00
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 で実装しているようです。
データの先読み
ServiceWorker の install 時に9記事分のページのデータが先読みされています。
つまりWEBページにアクセスした時にページの読み込みをしている「裏」で、ServiceWorker が9記事分のデータを取得してキャッシュに追加しているということです。
DNSプリフェッチ
これはページ読み込み時に外部ファイルにアクセスするより先にDNSの名前解決をしてしまうもので、100~200ms程度の時間がこれにより短縮できるとされています。
キャッシュ更新について(推測)
ここで気になるのが「キャッシュ更新のロジック」でブログというサービスの都合上、記事内容が更新されたり削除されたりすることは頻繁にありえる。
その場合キャッシュを保持していると、いつまでも更新前の記事を表示することになってしまうので、なんらかのタイミングでキャッシュを更新する必要があるはず。
ServiceWorker はおそらく動的に生成されており、それにより先読みするページの変更とキャッシュの更新を行っているようだが、具体的にどういうタイミングで更新をかけているのかは不明。
ServiceWorker が install されたタイミングで両者の更新を行っているようなので、記事が追加/更新されたタイミングで ServiceWorker が変更されて install -> キャッシュ更新というロジックで更新されるように推測される。
上記が正しければ、要するに ServiceWorker の更新とキャッシュの更新は一体化しており、キャッシュのみを更新するロジックはないということなる。
まとめ
爆速の主要因は「リクエスト数の少なさ」と「InstantClick」であるように見受けられる。
ちなみにキャッシュは先読みで最初の9記事だけをもっており、それ以外の記事についてはキャッシュを利用せずに純粋に InstantClick でのみ体感速度を向上させているようで、個人的には後続の記事をキャッシュしていないのは意外だった。
またこの記事ではフロント側に寄った説明をしているが、当然バックエンド側やサービスの設計も最適化されてるので、そもそもこのブログなら”素”でも速い。
実際にカテゴリ一覧や月別記事一覧から記事を閲覧するとキャッシュにない記事のはずだが、それでも十分高速さが体感できるので、フロント側の実装が通常のブログのものと同様であったとしても充分に高速であったはずだと思われる。
- 投稿日:2019-02-18T00:51:50+09:00
ES2018の正規表現の新機能を試してみた
参考
- New JavaScript Features That Will Change How You Write Regex
- あなたの知っている正規表現はもう古い! 正規表現の新常識(ES2018編) - Qiita
↑の内容を元に試しただけという内容ですので、正しい情報が欲しい方は参考ページを見て、本記事は見なくても良いです。
自分の備忘録として投稿させていただきました。
間違っている箇所などあれば、コメントなどよろしくお願いいたします
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現在
s (dotAll) flag for regular expressions
オプション無しのデフォルトでは、
.
(ドット)は改行文字にはマッチしないのですが、s
オプションをつけることで改行にもマッチします// sオプション無し -> false /foo.bar/.test(`foo bar`) // sオプション有り -> true /foo.bar/s.test(`foo bar`)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
から値を受け取れます。
\k
で使いまわすこともできる!const re = /(?<dup>ABC)x+\k<dup>/.test('ABCxxxABC'); // truereplaceでも使うことができる!
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 & WarRegExp Lookbehind Assertions
今まで先読みしかできなかったのですが、後読みの機能が追加されたようです。
console.log(/(?<=a)b/.test('ab')) // true console.log(/(?<!a)b/.test('cb')) // true以下のページも参考
情報ありがとうございます
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
とすると否定になるようです。
最後まで読んでいただきありがとうございましたm(_ _)m
- 投稿日:2019-02-18T00:51:22+09:00
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)
- 投稿日:2019-02-18T00:34:59+09:00
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 自身を含んで選択していることになる。後者の
setStart
とsetEnd
で指定している場合は、commonAncestorContainer
は自身の node だが、startContainer
/endContainer
はそれぞれsetStart
とsetEnd
で指定した node になる。これはユーザが手動で文字列を選択したときと同じような range オブジェクトになる。どちらが良いというよりかは、それぞれ場合によって使い分ける。
- 投稿日:2019-02-18T00:24:09+09:00
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)"
参考