20200404のJavaScriptに関する記事は26件です。

【knex】this.dialectに関してのエラー解決

エラー文

Using 'this.dialect' to identify the client is deprecated and support for it will be removed in the future. Please use configuration option 'client' instead.

解決法

var knex = require('knex')({
    dialect: 'mysql',
    connection: {
        host: 'localhost',
        user: 'root',
        password: '(パスワード)',
        database: '(データベース名)',
        charset: 'utf-8'
    }
});

上の文の、dialectをclientに変更する。

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

Vue.js で作るForm(フォーム)

Vue.jsで作るフォーム

form の input 要素 や textarea 要素、 select 要素に双方向 (two-way) データバインディングを作成するには、v-model ディレクティブを使用することができます。それは、自動的に入力要素のタイプに基づいて要素を更新するための正しい方法を選択します。ちょっと魔法のようですが、v-model はユーザーの入力イベントにおいてデータを更新するための基本的な糖衣構文 (syntax sugar) で、それに加えて、いくつかのエッジケースに対しては特別な配慮をしてくれます。

<参考文献>

https://jp.vuejs.org/v2/guide/forms.html

入力フォーム input (text)

HTML

<!-- v-model.lazyでinputからカーソルが離れた際に発火するようにする -->
<input id="title" type="text" v-model.lazy="eventData.title">
<pre>{{ eventData.title }}</pre> <!-- 確認 -->

JavaScript

export default {
  data(){
    return{
      eventData:{  //eventDataプロパティにtitleの初期値を設定
        title:'',
      }
    }
  }
}

入力フォーム input (number)

HTML

<!-- v-model.numberでvalueを数値に固定 -->
<input id="maxNumber" type="number" v-model.number="eventData.maxNumber"> 
<p>{{ typeof eventData.maxNumber }}</p> <!-- 確認 -->

JavaScript

export default {
  data(){
    return {
      eventData:{
        maxNumber: 0,
      }
    }
  }
}

input (先頭と後尾の改行を取り除く)

HTML

<!-- v-model.trimで改行を取り除く -->
<input id="host" type="text" v-model.trim="eventData.host"> 
<pre>{{ eventData.host }}</pre>

JavaScript

export default {
  data(){
    return{
      eventData:{
        host: ''
      }
    }
  }
}

チェックボックス(単体)

HTML

<input type="checkbox" id="isPrivate" v-model="eventData.isPrivate">
<label for="checkbox">非公開</label>
<p>{{ eventData.isPrivate }}</p>

JavaScript

export default {
  data(){
    return{
      eventData:{
        isPrivate: false, //boolean型で返ってきます
      }
    }
  }
}

チェックボックス(複数)

HTML

<input type="checkbox" id=10 value="10代" v-model="eventData.target"> 
<label for="10">10代</label>
<input type="checkbox" id=20 value="20代" v-model="eventData.target"> 
<label for="20">20代</label>
<input type="checkbox" id=30 value="30代" v-model="eventData.target"> 
<label for="30">30代</label>
<input type="checkbox" id=40 value="40代" v-model="eventData.target"> 
<label for="40">40代</label>
<p>{{ eventData.target }}</p>

JavaScript

export default {
  data(){
    return{
      eventData:{
        target: [], //配列で指定
      }
    }
  }
}

ラジオボタン

HTML

<input type="radio" id="free" value="free" v-model="eventData.price">
<label for="free">無料</label>
<input type="radio" id="paid" value="paid" v-model="eventData.price">
<label for="paid">有料</label>
<p>{{ eventData.price }}</p>

JavaScript

export default {
  data(){
    return{
      eventData:{
        price: "free"
      }
    }
  }
}

セレクトボックス

HTML

<select v-model="eventData.location" multiple>
<option v-for="location in locations" v-bind:key="location">
{{ location }}</option>
</select>
<p>{{ eventData.location }}</p>

JavaScript

export default {
  data(){
    return {
      locations: ["東京", "埼玉", "千葉", "神奈川"],
       eventData:{
         location: []
       }
    }
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Instagramでスクロール中にモーダルが表示され閲覧できなくなる問題を解決する。

はじめに

※ 悪用厳禁です。

我々のようなエンジニア(隠キャ)はInstagramという(陽キャ御用達の)サービスを使う機会がないですし、アカウントも作成していないと思います。

しかし、自分の大好きな芸能人の写真をInstagramでニヤニヤしながら見ていると突如、モーダルが表示され、モーダルを閉じることもスクロールすることもできなくなり閲覧できない状態になってしまいます。

その名も、「Instagram問題

大概の人は、そこで諦めるかもしれません。
でも、我々は諦めません。この問題を解決する術があるからです。

該当する問題

ログインをしていないときに、しばらくスクロールすると次のような画面になります。
この状態になると、半透明になっているところをクリックしてもモーダルは閉じませんし、スクロールもできません。

写真は有名ユーチューバーHikakinのInstagramを使用しています。
スクリーンショット 2020-04-04 17.48.09.png

あなたなら、このInstagram問題をどう解決しますか?
答えは簡単です。

次の3つのステップを順に行えば、アカウントを持っていなくても、自由に好きな芸能人の写真をみることができます。

Step1 デベロッパーツールを表示する

これは簡単ですね。option + command + iを押せば表示されます。
設定によって表示される位置は異なりますが、以下のように表示されればオッケーです。

スクリーンショット 2020-04-04 18.04.31.png

Step2 モーダルを消す

まず、モーダルを表示しているタグを探します。
デベロッパーツールをElementsタブの状態のまま、モーダル上で右クリック->検証をクリックすると、該当するタグが示されます。
青くハイライトされているところが、モーダルを表示しているタグです。

スクリーンショット 2020-04-04 18.09.07.png

右クリック->Delete elementをクリックするとモーダルの表示が消えます。

しかし、まだスクロールができません。

Step3 スクロールを可能にする。

スクロールができない原因を考えます。
CSSでは、overflow: hidden;を使うとスクロールができなくなるので、このCSSが指定されている要素を探します。
Instagramではbodyタグのstyle属性に指定されていました。

スクリーンショット 2020-04-04 22.39.39.png

bodyタグ上で右クリック->Edit attributeをクリックすると、属性が編集できるようになるので、style属性を削除しましょう。

おわり

無事に、アカウントを登録することなく自由にInstagramを閲覧できるようになりました。これで、あなたは心配することなく思う存分楽しむことができます。

おまけ

紹介した3ステップは全てボタンをクリックして行いました。よりエンジニアらしくするために、コードで再現しましょう。

まずは、デベロッパーツールを開いて、表示されるモーダルのclass名かId名を確認します。先ほどの写真からclass名はRnEpoであることがわかります。

準備ができたので、まずはモーダルを削除しましょう。

const modal = document.getElementsByClassName('RnEpo')[0];
modal.parentNode.removeChild(modal);

次に、スクロールを可能にしましょう。
bodyタグのstyle属性を削除します。

document.body.removeAttribute('style');
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでタブ機能を実装する(シンプル、IE対応)

はじめに

シンプルなタブの実装を速やかに終わらせるための備忘録。
CSSは最低限のデザインだけです。クラス名も好きに変えちゃってください。

ソースコード

index.html
    <!-- タブメニュー -->
    <ul>
      <li class="tab active">Portfolio</li>
      <li class="tab">Jobs</li>
      <li class="tab">Design</li>
      <li class="tab">Event</li>
    </ul>
    <!-- タブコンテンツ -->
    <div class="panel show">Portofolioです</div>
    <div class="panel">Jobsです</div>
    <div class="panel">Designです</div>
    <div class="panel">Eventです</div>

style.css
/* 子要素の.tabをfloatで左に浮かせたので親要素の高さを確保する */
ul:after{
  content: '';
  display: block;
  clear: both;
}
/* タブメニューのデザイン */
.tab{    
  padding: 5px 7px;
  text-align:center;
  display:block;
  float:left;
}
/* 選択中のタブの色を変える */
.tab.active{
  background: #000;
  border-radius: 3px;
  color:#FFF;
  transition: all 0.5s ease-out;
}
/* .showがついていない.panelは全て非表示 */
.panel {
  display: none;
}
/* .showがついた.panelを表示する */
.panel.show {
  display: block;
}
main.js
  // worksページタブ機能
  const tabs = document.getElementsByClassName('tab');

  for(let i = 0; i < tabs.length; i++) {
    tabs[i].addEventListener('click', tabSwitch);
  }
  // タブをクリックすると実行する関数
  function tabSwitch(){
    // .tabを名付けた要素のクラスを付け替える処理
    document.getElementsByClassName('active')[0].classList.remove('active');
    this.classList.add('active');

    // コンテンツのclassの値を変更
    document.getElementsByClassName('show')[0].classList.remove('show');
    const arrayTabs = Array.prototype.slice.call(tabs);
    const index = arrayTabs.indexOf(this);
    document.getElementsByClassName('panel')[index].classList.add('show');
  };

解説

getElementsByClassName('active')[0]

getElementsByClassName()
・対象となるクラス名が設定されているHTML要素をすべて取得できる

なぜ[0]をつけるのか?

与えられたクラス名で得られる子要素すべての配列ライクのオブジェクトを返します。documentオブジェクトで呼び出されたときは、ルートノードを含む、完全な文書が検索されます。
https://developer.mozilla.org/ja/docs/Web/API/Document/getElementsByClassName

よって配列を個別で取得する必要がある。この場合は、.activeがついている要素の1番目を取得している。(上のソースコードであれば、.activeは1つしかないので[0]となる)

console.log(getElementsByClassName('active'));
//HTMLCollection []
// length: 1
// 0: li.tab.active <= ここから.activeの要素を取得する
// __proto__: HTMLCollection

classList

・対象要素に設定しているクラスを配列のように扱えるオブジェクト
・読み取り専用のプロパティ
Element.classList.メソッドでよく使われる。

メソッド名 機能
add() クラスを追加
remove() クラスを削除
toggle() クラスがあれば削除・無ければ追加
contain() クラス名の有無を true / false で返す
replace( oldClass, newClass ) oldClassをnewClassで置き換え

Array.prototype.slice.call(tabs)

tabsはHMTLCollectionという配列風オブジェクトなので、配列に変換する。

arrayTabs.indexOf(this)

・String オブジェクトの中からfromIndexで1番最初に現れた値のインデックスを返す。
・値が見つからない場合は-1を返す。
・document.getElementsByClassName('tab')で取得したHTMLCollectionをArray.prototype.slice.call()で配列に変換したので、indexOf()でインデックスを取得し、.showをつける.panelを判別する。

おわりに

間違いありましたらご指摘お願いします!

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

【JavaScript】thisが指すオブジェクトがわけわからなくなるので書いておく

はじめに

JavaScriptを始めた時に、一番最初につまづくポイントってthisば気がします。
自分も訳わかりませんでした。
そんな時のためにチラッと確認できるようここにまとめておきます。

開眼!JavaScriptを参考にしています。

結論

関数が実行された時に、thisは設定されています。
その呼び出された関数をプロパティかメソッドとして保持しているオブジェクトがthisに設定されています。

コードにしてみる

const people = {
  name: 'java男',
  age: 23,
  greet: function () {
    console.log(`I am ${this.name}`)
  }
}

people.greet()
// 出力 : I am java男

ここでいうプロパティかメソッドとして保持しているオブジェクトとは、peopleオブジェクトとなります。
つまり、this.nameが指すのは、peopleオブジェクトのnameプロパティとなります。

const testObj = {
  test: 'test'
}

const thisTest = function () {
  console.log(`${this}`)
}

testObj.thisTest = thisTest

// (1) [object Object]
testObj.thisTest()

// (2) [object global]
thisTest()

(1)のthisはtestObjオブジェクトを指しています。なぜなら、thisTestをtestObjのメソッドとして定義したからです。
対して(2)のthisはグローバルオブジェクトを指しています。なぜなら、ここで実行されているthisTestを保持しているのはグローバルオブジェクトであるためです。

終わり

アロー関数ではthisをグローバルに束縛するとか他にもありますが、とりあえずごちゃつくのでここで終わります。
ややこしくなりがちがthis、きっちり学んでいきましょう。

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

JavaScriptの繰り返し(ループ)処理について

様々な繰り返し処理がある中で、今回はfor文について説明していきます。

for文による繰り返しは、指定された条件がfalse(偽)となるまで繰り返されます。
for文の基本形は以下のようになっています。

for(初期化値; 条件式; 加算式){

(繰り返し処理)

}

具体的な例を挙げますと、
for (let i = 0; i < 10; i++) {
console.log(i);
}

これは、変数iを0で初期化し、ループが1回回るごとに変数iに1を足していく。変数iが10未満の間ループを実行する。そしてその結果をコンソールに出力させる。という意味になります。

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

jQueryのappendで取得元が消えてしまうときの回避方法

はじめに

エレメントから要素を取得して、その要素を別の場所にコピーする処理をjQueryのappendでしようとしていたのですが、取得元が消えてしまいハマってしまったので、その回避策のメモです。

事象

以下のような画面があったとして、「テストタイトル1」をクリックすると、「テストタイトル2」の下に「テスト内容」がコピーされるプログラムを作成したかったとします。

テストタイトル1 // ① クリック
テスト内容
テストタイトル2 // ② この下に「テスト内容」がコピーされる

そこで、以下のようなHTMLとJavascriptを実装します。

html
<html>
  <head>
    <meta charset="utf-8">
    <title>テスト</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
  </head>
  <body>
  <div id="test" class="test">
    <div class="test-title-1">
      テストタイトル1
      <div class="test-contents">テスト内容</div>
    </div>
    <div class="test-title-2">
      テストタイトル2
    </div>
  </div>
  </body>
  <script>
    $('.test-title-1').on('click', function(){
      const target = event.currentTarget; // クリックした要素(test-title-1)を取得
      const testContents = $(target).find('.test-contents'); // test-contentsクラスの要素取得
      $('.test-title-2').append(testContents); // test-title-2クラスへappend
    });
  </script>
</html>

すると、なぜか結果は以下のようになり、「テストタイトル1」直下のテスト内容が消えてしまいます。

テストタイトル1
テストタイトル2
テスト内容

原因

jQueryオブジェクトのappend()は追加or移動という仕様でした。

公式サイトを見ると、以下のように記載がありました。

If an element selected this way is inserted into a single location elsewhere in the DOM, it will be moved into the target (not cloned)
(日本語訳)
「この方法で選択された要素がDOMの他の場所の単一の場所に挿入された場合、それはターゲットに移動されます」

つまり、取得したDOMはappendすると、別の要素へ移動されてしまうのです。。。

回避策

これを回避するには、appendする時に、変数を複製すればOKです。

先ほどのサンプルプログラムを利用すると、具体的には、clone()メソッドを以下のように使います。

html
<html>
  <head>
    <meta charset="utf-8">
    <title>テスト</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
  </head>
  <body>
  <div id="test" class="test">
    <div class="test-title-1">
      テストタイトル1
      <div class="test-contents">テスト内容</div>
    </div>
    <div class="test-title-2">
      テストタイトル2
    </div>
  </div>
  </body>
  <script>
    $('.test-title-1').on('click', function(){
      const target = event.currentTarget;
      const testContents = $(target).find('.test-contents').clone(); // ここにclone()を追加
      $('.test-title-2').append(testContents);
    });
  </script>
</html>

すると、取得した要素は一度クローン(複製)されるため、appendが移動するのは複製した要素となるので、以下のように正常にコピーできます。

テストタイトル1 // ① クリック
テスト内容
テストタイトル2
テスト内容 // ② テスト内容が正常にコピーされる

さいごに

2020年から個人ブログはじめました!

フリーランスエンジニアになって得た知識と経験をもとに、フリーランスエンジニアに関する情報をはじめ、IT技術情報や業界情報、エンジニアライフハック等のコンテンツを配信していく予定です。

まだまだ記事数は少ないのですが、週単位で更新してますので、もしご興味ございましたら、みていただけると嬉しいです。

https://yacchi-engineer.com/

さいごまでお読みいただき、ありがとうございました。

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

問いかけると柴犬の画像を返してくれるLINE botを作ってみた

概要

 LINE botでできることを調べていたら、画像を返すことが可能とのことで試してみました。
ただ画像を返すだけでは面白くないので、柴犬APIで画像を拾ってきて表示することにしました。最後にソースコードの全体を載せています。

 柴犬APIについてはこちらをご参照ください。実際の動きは次のようになります。

【デモ】

構成

 主な構成は次の通りです。LINE botからテキストを送ると、ローカルにあるNode.jsサーバにWebhookが投げられ、LINE Messaging APIのフォーマットに従って画像URlが返されます。

画像送信の構成図.jpg

作ってみる

開発環境

OS:Windows 10
Node.js:v10.15.3

【ライブラリバージョン】
@line/bot-sdk:6.8.4
express:4.17.1
axios:0.19.2

プログラム解説

プログラムの全体は最後に載せるとして、ここでは重要なところのみ解説します。
LINE botのサーバで、画像を返すフォーマットは次の通りです。

return client.replyMessage(event.replyToken, {
    type: 'image',
    originalContentUrl: 'オリジナルサイズの画像URL', 
    previewImageUrl: 'LINEアプリのトーク画面にプレビューされるサイズの画像URL'
  });

 画像URLを指定するため、GoogleドライブやDropboxなどのストレージを使う必要があります。
 また、使用できる画像サイズに限度があるようです。詳しくはこちらのLINE Developerサイトを参照してください。

 今回の例では、次のように柴犬APiを利用して得られた画像URLを、固定で入力しています。

return client.replyMessage(event.replyToken, {
    type: 'image',
    originalContentUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg', 
    previewImageUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg'
  });

余談

 ここまで至るにも苦労しました(;'∀')
なぜか画像が送れないなーと色々悩んでいて、最終的にはDiscordのProtoOutStudioの技術質問チャンネルに投げかけました。嬉しいことに2期生の方が動作確認して誤り個所を教えていただきました。本当に感謝しています。
 結局は返すフォーマットが間違っていただけなんて....。次からはもっとちゃんと仕様を読んで理解せねば!

 と、いうことで画像を送ることができました。次は自宅で稼働している菜園管理システムとつなげて、画像を一定時間ごとに送れるようにしていこうと思います。

参考

LINE Messaging API でできることまとめ【送信編】

ソースコード

'use strict';

const axios = require('axios');         // 追加
const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3000;

const config = {
    channelSecret: 'LINE botのチャンネルシークレット',
    channelAccessToken: 'LINE botのアクセストークン'
};

const app = express();

app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)
app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);

    //ここのif文はdeveloper consoleの"接続確認"用なので後で削除して問題ないです。
    if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){
        res.send('Hello LINE BOT!(POST)');
        console.log('疎通確認用');
        return; 
    }

    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) {

  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  return client.replyMessage(event.replyToken, {
    type: 'image',
    originalContentUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg', 
    previewImageUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg'
  });
}
app.listen(PORT);
console.log(`Server running at ${PORT}`);

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

LINE botから画像送信~問いかけると柴犬の画像を返してくれるLINE botを作ってみた

概要

 LINE botでできることを調べていたら、画像を返すことが可能とのことで試してみました。
ただ画像を返すだけでは面白くないので、柴犬APIで画像を拾ってきて表示することにしました。最後にソースコードの全体を載せています。

 柴犬APIについてはこちらをご参照ください。実際の動きは次のようになります。

【デモ】

構成

 主な構成は次の通りです。LINE botからテキストを送ると、ローカルにあるNode.jsサーバにWebhookが投げられ、LINE Messaging APIのフォーマットに従って画像URlが返されます。

画像送信の構成図.jpg

作ってみる

開発環境

OS:Windows 10
Node.js:v10.15.3

【ライブラリバージョン】
@line/bot-sdk:6.8.4
express:4.17.1
axios:0.19.2

プログラム解説

プログラムの全体は最後に載せるとして、ここでは重要なところのみ解説します。
LINE botのサーバで、画像を返すフォーマットは次の通りです。

return client.replyMessage(event.replyToken, {
    type: 'image',
    originalContentUrl: 'オリジナルサイズの画像URL', 
    previewImageUrl: 'LINEアプリのトーク画面にプレビューされるサイズの画像URL'
  });

 画像URLを指定するため、GoogleドライブやDropboxなどのストレージを使う必要があります。
 また、使用できる画像サイズに限度があるようです。詳しくはこちらのLINE Developerサイトを参照してください。

 今回の例では、次のように柴犬APiを利用して得られた画像URLを、固定で入力しています。

return client.replyMessage(event.replyToken, {
    type: 'image',
    originalContentUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg', 
    previewImageUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg'
  });

余談

 ここまで至るにも苦労しました(;'∀')
なぜか画像が送れないなーと色々悩んでいて、最終的にはDiscordのProtoOutStudioの技術質問チャンネルに投げかけました。嬉しいことに2期生の方が動作確認して誤り個所を教えていただきました。本当に感謝しています。
 結局は返すフォーマットが間違っていただけなんて....。次からはもっとちゃんと仕様を読んで理解せねば!

 と、いうことで画像を送ることができました。次は自宅で稼働している菜園管理システムとつなげて、画像を一定時間ごとに送れるようにしていこうと思います。

参考

LINE Messaging API でできることまとめ【送信編】

ソースコード

'use strict';

const axios = require('axios');         // 追加
const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3000;

const config = {
    channelSecret: 'LINE botのチャンネルシークレット',
    channelAccessToken: 'LINE botのアクセストークン'
};

const app = express();

app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)
app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);

    //ここのif文はdeveloper consoleの"接続確認"用なので後で削除して問題ないです。
    if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){
        res.send('Hello LINE BOT!(POST)');
        console.log('疎通確認用');
        return; 
    }

    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) {

  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  return client.replyMessage(event.replyToken, {
    type: 'image',
    originalContentUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg', 
    previewImageUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg'
  });
}
app.listen(PORT);
console.log(`Server running at ${PORT}`);

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

TypeScriptで学ぶデザインパターン〜Template Method編〜

対象読者

  • デザインパターンを学習あるいは復習したい方
  • TypeScriptが既に読めるあるいは気合いで読める方
    • いずれかのオブジェクト指向言語を知っている方は気合いで読めると思います
  • UMLが既に読めるあるいは気合いで読める方

環境

  • OS: macOS Mojave
  • Node.js: v12.7.0
  • npm: 6.14.3
  • TypeScript: Version 3.8.3

本シリーズ記事一覧(随時更新)

Template Methodパターンとは

処理の大枠を持たせるためのパターンです。

"処理の大枠"というのがテンプレートです。処理の流れをざっくり規定して、その流れに沿って具体的な処理を埋め込んでいくことができます。

サンプルコード

Template Methodパターンで作られたクラス群がどんなものになるのか確認していきましょう。

今回は、題材として"エディターのヘルプ"を想定します。GitHubにも公開しています。

「Markdownで太字にする場合は**__で文字列で囲う」といったヘルプを表示させるような機能を作っていきます。

modules/EditorExample.ts

エディターのヘルプを表示させるためのテンプレートを示す抽象クラスです。

EditorExample.ts
export default abstract class EditorExample {
  type: string;
  text: string;

  abstract strong(): string;
  abstract italic(): string;

  showExample(): void {
    console.log('種別が' + this.type + 'の場合の記述例は以下です。');
    console.log('太字: ' + this.strong());
    console.log('斜体: ' + this.italic());
  }
}

strongメソッドとitalicメソッドは本クラスのサブクラスで実装します。
showExampleメソッドがテンプレートで、大まかな処理の流れを規定しています。

modules/HtmlEditorExample.ts

テンプレートの実装クラスです。

HtmlEditorExample.ts
import EditorExample from './EditorExample';

export default class HtmlEditorExample extends EditorExample {
  type: string = 'HTML';
  text: string;

  constructor(text: string) {
    super();
    this.text = text;
  }

  strong(): string {
    const strongExample: string = '<strong>' + this.text + '</strong>';

    return strongExample;
  }

  italic(): string {
    const italicExample: string = '<i>' + this.text + '</i>';

    return italicExample;
  }
}

strongメソッドとitalicメソッドを実装していることを確認してください。

modules/MarkdownEditorExample.ts

テンプレートの実装クラスです。HtmlEditorExampleクラスと立ち位置は同じです。

MarkdownEditorExample.ts
import EditorExample from "./EditorExample";

export default class MarkdownEditorExample extends EditorExample {
  type: string = 'Markdown';
  text: string;

  constructor(text: string) {
    super();
    this.text = text;
  }

  strong(): string {
    const strongExample: string = this.getStrongExample();

    return strongExample;
  }

  italic(): string {
    const italicExample: string = this.getItalicExample();

    return italicExample;
  }

  private getStrongExample(): string {
    const exampleNotations: string[] = [
      '**',
      '__'
    ];

    let strongExample: string = '';

    for (let exampleNotation of exampleNotations) {
      strongExample += exampleNotation + this.text + exampleNotation;
      if (exampleNotation !== exampleNotations[exampleNotations.length - 1]) {
        strongExample += ', ';
      }
    }

    return strongExample;
  }

  private getItalicExample(): string {
    const exampleNotations: string[] = [
      '*',
      '_'
    ];

    let italicExample: string = '';

    for (let exampleNotation of exampleNotations) {
      italicExample += exampleNotation + this.text + exampleNotation;
      if (exampleNotation !== exampleNotations[exampleNotations.length - 1]) {
        italicExample += ', ';
      }
    }

    return italicExample;
  }
}

strongメソッドとitalicメソッドを実装していることを確認してください。

Main.ts

Template Methodパターンで作られたクラス群を実際に使う処理が書かれています。

Main.ts
import EditorExample from './modules/EditorExample';
import HtmlEditorExample from './modules/HtmlEditorExample';
import MarkdownEditorExample from './modules/MarkdownEditorExample';

const htmlEditorExample: EditorExample = new HtmlEditorExample('こんにちは!');
htmlEditorExample.showExample();

const markdownEditorExample: EditorExample = new MarkdownEditorExample('こんにちは!');
markdownEditorExample.showExample();

showExampleメソッドでテンプレートを実行していることを確認してください。

クラス図

ここまでTemplate Methodパターンで作られたクラス群を1つずつ確認してきました。次にクラス図を示します。Template Methodパターンの全体像を整理するのにお役立てください。

TemplateMethod.png

解説

最後に、このデザインパターンの存在意義を考えます。

EditorExample抽象クラスが存在しない場合を考えてみます。つまり、showExampleメソッドのようなメソッドをHtmlEditorExampleクラスとMarkdownEditorExampleクラスそれぞれに定義している場合を考えてみます。もし、showExampleメソッドのようなメソッドを修正する必要があったらHtmlEditorExampleクラスとMarkdownEditorExampleクラス両方を修正する必要が生じてしまいます。何が言いたいかというと、EditorExample抽象クラスにshowExampleメソッドを定義することでロジックを共通化することができるのです。

補足

サンプルコードの実行方法はこちらと同様です。

参考

あとがたり

EditorExample抽象クラスのshowExampleメソッドにJavaでいうところのfinalを付与させたかったけど今のところやり方がわからず...。できないわけないと思うんだけどな。

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

LINE BOTで天気を返すサンプルがngrokで動いてnowで動かない件

こちらのサンプルがngrokで動いて、Nowだとうまく動かない件の対応。

axiosを使って別のサーバーにリクエストを出してるので非同期処理のあたりが怪しいですね。

もとのコード

これだとngrokでうまく動くけど、now上でうまく動かないというレポート

server.js


省略



function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let mes = ''
  if(event.message.text === '天気教えて!'){
    mes = 'ちょっとまってね'; //待ってねってメッセージだけ先に処理
    getNodeVer(event.source.userId); //スクレイピング処理が終わったらプッシュメッセージ
  }else{
    mes = event.message.text;
  }

  return client.replyMessage(event.replyToken, {
    type: 'text',
    text: mes
  });
}

const getNodeVer = async (userId) => {
    const res = await axios.get('http://weather.livedoor.com/forecast/webservice/json/v1?city=400040');
    const item = res.data;

    await client.pushMessage(userId, {
        type: 'text',
        text: item.description.text,
    });
}



省略


書き換え

replyとpushのタイミングを変えてみる。

server.js


省略



async function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  //"天気教えて"以外の場合は反応しない
  if(event.message.text !== '天気教えて') {
    return client.replyMessage(event.replyToken, {
      type: 'text',
      text: '"天気教えて"と言ってね'
    });
  }

  let mes = '';
  mes = 'ちょっと待ってね'; //"ちょっと待ってね"ってメッセージだけ先に処理
  await client.replyMessage(event.replyToken, {
      type: 'text',
      text: mes
  });

  //axiosを使って天気APIにアクセス
  const CITY_ID = `400040`; //ライドアのAPIから取得したいシティのIDを
  const URL = `http://weather.livedoor.com/forecast/webservice/json/v1?city=${CITY_ID}`;
  const res = await axios.get(URL);
  const item = res.data;
  return client.pushMessage(event.source.userId, {
      type: 'text',
      text: item.description.text,
  });
}



省略


おまけ: 関数分けサンプル

関数に分けるとこんな感じ。

server.js


省略



async function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  //"天気教えて"以外の場合は反応しない
  if(event.message.text !== '天気教えて') {
    return client.replyMessage(event.replyToken, {
      type: 'text',
      text: '"天気教えて"と言ってね'
    });
  }

  let mes = '';
  mes = 'ちょっと待ってね'; //"ちょっと待ってね"ってメッセージだけ先に処理
  await client.replyMessage(event.replyToken, {
      type: 'text',
      text: mes
  });

  const CITY_ID = `400040`; //ライドアのAPIから取得したいシティのIDを
  return getWeather(event.source.userId, CITY_ID);
}

const getWeather = async (userId, CITY_ID) => {
  //axiosを使って天気APIにアクセス
  const URL = `http://weather.livedoor.com/forecast/webservice/json/v1?city=${CITY_ID}`;
  const res = await axios.get(URL);
  const item = res.data;
  return client.pushMessage(userId, {
      type: 'text',
      text: item.description.text,
  });
}



省略


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

CKeditor4と画像アップロード(CKfinder使用しない)

行ったこと

  • Cakephp にCKeditor4を適用
  • 画像アップロードの処理をコントローラに書く

CKeditor適用

公式サイト

View(ctp)ファイル

cdn 経由で呼び出し
※Fullバージョンがおすすめです。

<?= $this->Html->script('https://cdn.ckeditor.com/4.14.0/full/ckeditor.js') ?>

※コントローラ指定

var url = '<?= $this->Url->build(['controller' => 'ImgUpload', 'action' => 'index']) ?>';
var editor = CKEDITOR.replace('editor', {
    language: 'ja',
    toolbarCanCollapse: true,
    filebrowserUploadMethod: 'form',
    filebrowserUploadUrl: url, /* 上で指定したurl(変数) */
    image_previewText: '画像アップロード',
};

ツールバーのオプションなどは今回省略します。

画像アップロード処理

上で指定したImgUploadControllerの内容

namespace App\Controller\Admin;
use App\Controller\AppController;

class ImgUploadController extends AppController
{
public function index()
    {
        if (isset($_FILES['upload']['name'])) {

            $uploaddir = WWW_ROOT;
            $file = $_FILES['upload']['tmp_name'];
            $file_name = $_FILES['upload']['name'];

            $file_name_array = explode(".", $file_name);
            $extension = end($file_name_array);
            $new_image_name = rand() . '.' . $extension;
            chmod($uploaddir.'/uploads', 0777);
            $allowed_extension = array("jpg", "gif", "png");

            if (in_array($extension, $allowed_extension)) {
                move_uploaded_file($file, $uploaddir.'/uploads/'.$new_image_name);
                $function_number = $_GET['CKEditorFuncNum'];
                $url = '/uploads/'.$new_image_name;
                $message = '';
                echo "<script type='text/javascript'>window.parent.CKEDITOR.tools.callFunction($function_number, '$url', '$message');</script>";
                return;
            }
        }
    }
}
webroot
  ∟ uploads
       ∟ ここに画像ファイルが保存される

参照サイト: https://www.webslesson.info/2019/01/uploading-image-in-ckeditor-with-php.html

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

[JavaScript] Arrayメソッド破壊・非破壊チートシート

JSのArrayメソッドで破壊非破壊がまとまっている表が欲しいな〜〜
と思ったので作りました。

チートシート

メソッド名 破壊 非破壊 用途        
splice()      配列から配列の一部を取り出し、新しい配列を返す
slice()     配列から配列の一部を取り出し、新しい配列を返す
push() 配列の末尾に要素を追加し、新たな配列の長さを返す
unshift() 配列の先頭に要素を追加し、新たな配列の長さを返す
pop() 配列の最後の要素を取り除き、その値を返す
shift() 配列の先頭の要素を取り除き、その値を返す
filter() 引数として与えられた関数を各配列要素に対して実行し、それに合格したすべての配列要素からなる新しい配列を返す
reduce() 引数として与えられた関数を(左から右へ)各配列要素に対して実行し、単一の値にして返す
reduceRight() 引数として与えられた関数を(右から左へ)各配列要素に対して実行し、単一の値にして返す
map() 配列内の各要素を引数として与えられた関数で順番に加工し、新しい配列を返す
concat() 配列に他の配列や値をつないでできた新しい配列を返す
find() 引数として与えられた関数を満たす配列内の最初の要素の値を返す
findIndex() 引数として与えられた関数を満たす場合、配列内の インデックス を返す
indexOf() 引数に与えられたと同じ値を持つ配列要素の内、最初のものの添字を返す
includes() 引数の値が配列に含まれているかどうかを true または false で返す
sort() 配列の要素をソートする                   
reverse() 配列の要素を反転させ、配列を書き換える   
join() 配列の全要素を順に引数の文字列で連結した文字列を新たに作成して返す。区切り文字や指定されない場合、デフォルトは,になる
fill() 配列中の開始インデックスから終了インデックスまで固定値に変更する
copyWithin() サイズを変更せずに、配列の一部を同じ配列内の別の場所にシャローコピーして返す

最後に

関数自体を詳しく説明はしていないので、必要があればMDN web docsをご参考ください。
今後も追加していきますが一旦。

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

nowのデプロイで古い書き方からのマイグレートメモ

now.shを使うときにv2系の書き方でも警告が出るようになってますね。
もう1系は使えないのかも。

nowで詰まった人がいたのでメモ。

今まで書いてたやり方と修正点

これまでは、これでよかったのですが......

now.json
{
    "version": 2,
    "name": "mylinebot",
    "builds": [{ "src": "server.js", "use": "@now/node" }],
    "routes": [
        { "src": "/", "dest": "server.js" },
        { "src": "/webhook", "dest": "server.js" }
    ]
}
now --target production

nameプロパティの注意

まずはここ。

❗️  The `name` property in now.json is deprecated (https://zeit.ink/5F)

ここを読むと、書いてますが

NOTE: The name property has been deprecated in favor of Project Linking, which allows you to link a ZEIT Now Project to your local codebase when you run now.

nameプロパティが非推奨と言われます。

なのでnameプロパティを外します。修正版はこちら。

now.json
{
    "version": 2,
    "builds": [{ "src": "server.js", "use": "@now/node" }],
    "routes": [
        { "src": "/", "dest": "server.js" },
        { "src": "/webhook", "dest": "server.js" }
    ]
}

デプロイコマンド

次にここです。

WARN! We recommend using the much shorter `--prod` option instead of `--target production` (deprecated) 

もともとの書き方のnow --target productionnow --prodで良いよと言われます。短い方が良いですね。

now --prod

これでOKです。

おまけ: 実行時

実際のデプロイで表示されるコンソールの紹介です。

対話的に質問されます。"deployの設定をしますか?"的な質問です。エンターかYをタイプして進みましょう。

Now CLI 17.1.1
? Set up and deploy “~/Documents/ds/playground/mylinebot”? [Y/n] ←ここでエンターもしくはY

次にデプロイ先のアカウントを選択。 たぶんチームアカウントとかあると選択肢に載ってくるんだと思いますが、たぶん最初は自分のアカウントだけなので自分のアカウントが表示されるのを確認したらエンター。

? Which scope do you want to deploy to? 
● n0bisuke ←ここでエンター

次になんて名前でデプロイするか聞かれます。package.jsonのnameプロパティに書いてある名前が表示されるので、エンターかYをタイプして進みます。

? Found project “n0bisuke/mylinebot”. Link to it? [Y/n] ←ここでエンターもしくはY

これでデプロイできるはず......!

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

【Rails】Ajaxを用いた非同期いいね機能の実装

目標

ezgif.com-video-to-gif.gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

ログイン機能、投稿機能を実装済み。

いいね機能の実装

テーブル

usersテーブル

    カラム          データ型     
name string
introduction text
profile_image_id string

booksテーブル

    カラム          データ型     
title string
body text
book_id integer
user_id integer

favoritesテーブル

    カラム          データ型     
user_id integer
book_id integer

モデル

user.rb
    has_many :favorites, dependent: :destroy

    def favorited_by?(book)
        favorites.where(book_id: book.id).exists?
    end
book.rb
    has_many :favorites, dependent: :destroy
favorite.rb
    belongs_to :user
    belongs_to :book

ルーティング

routes.rb
    resources :books do
        resource :favorites, only: [:create, :destroy]
    end

コントローラー

favorites.controll.rb
class FavoritesController < ApplicationController
    def create
        @book = Book.find(params[:book_id])
        # いいねボタンを連打しても1回しかいいね出来ない様に条件をつける
        unless current_user.favorited_by?(@book)
            favorite = current_user.favorites.new(book_id: @book.id)
            favorite.save
            redirect_to @book
        end
    end

    def destroy
        @book = Book.find(params[:book_id])
        favorite = current_user.favorites.find_by(book_id: @book.id)
        favorite.destroy
        redirect_to @book
    end
end

ビュー

1. 投稿一覧をパーシャル化

books/index.html.erb
    <% @books.each do |book| %>
        <tr>
            <%= render 'books', book: book %>
        </tr>
    <% end %>

3. showページ等でもいいねボタンを使い回したいので、さらにいいねボタンをパーシャル化

books/_books.html.erb
    <td>
        <%= render 'favoritebutton', book: book %>
    </td>

4. いいねボタンを作成

books/_favoritebutton.html.erb
# current_userがその投稿をいいねしているかによって表示を変えている
<% if book.favorited_by?(current_user) %>
    <%= link_to book_favorites_path(book), method: :delete, remote: true do %>
    <i class="fa fa-heart" aria-hidden="true" style="color: red;"></i>
    <%= book.favorites.count %>
    <% end %>
<% else %>
    <%= link_to book_favorites_path(book), method: :post, remote: true do %>
    <i class="fa fa-heart-o" aria-hidden="true"></i>
    <%= book.favorites.count %>
    <% end %>
<% end %>

非同期機能の実装

1. jQueryの導入

Gemfile
    gem 'jquery-rails'
ターミナル
    $ bundle
application.js
    //= require rails-ujs
    //= require activestorage
    //= require turbolinks
    //= require jquery
    //= require_tree .

2. いいね後のジャンプ先を削除

favorites.controll.rb
class FavoritesController < ApplicationController
    def create
        @book = Book.find(params[:book_id])
        unless current_user.favorited_by?(@book)
            favorite = current_user.favorites.new(book_id: @book.id)
            favorite.save
        end
    end

    def destroy
        @book = Book.find(params[:book_id])
        favorite = current_user.favorites.find_by(book_id: @book.id)
        favorite.destroy
    end
end

3. パーシャルの親要素にクラスをつける

books/_books.html.erb
    #eachで呼び出されている各投稿のいいねボタンに対して、それぞれ一意のクラスを付けている
    <td class="favoritebutton_<%= book.id %>">
        <%= render 'favoritebutton', book: book %>
    </td>

4. JavaScriptファイルの作成

favorites/create.js.erb
$(".favoritebutton_<%= @book.id %>").html("<%= j(render 'books/favoritebutton', book: @book ) %>");
favorites/destroy.js.erb
$(".favoritebutton_<%= @book.id %>").html("<%= j(render 'books/favoritebutton', book: @book ) %>");

$(".favoritebutton_<%= @book.id %>")
➡︎「3」で付けたクラスを指定

.html("<%= j(render 'books/favoritebutton', book: @book ) %>");
➡︎いいねボタンのパーシャルをrenderしている

参考サイト

https://freecamp.life/rails-favorite-ajax/

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

JavascriptでCSS (Sass) プロパティを利用する

この記事は https://css-tricks.com/getting-javascript-to-talk-to-css-and-sass/ の翻訳記事となります。
(2020/4/3 寄稿記事)

Screen Shot 2020-04-04 at 11.00.07.png

JavaScriptとCSSは20年以上にわたって依存し合っているわけですが、いまだ相互のデータのやりとりには大変な苦労があります。

もちろん、これまでも数多くのアプローチが紹介されていますが、ここではシンプルかつ直感的なアプローチを紹介したいと思います。
ここで言うシンプルかつ直感的と言うのは、構造を変えての試みということではなく、CSSカスタムプロパティやSass変数の利用について述べることにします。

CSSカスタムプロパティとJavaScript

カスタムプロパティというものはそうたいそうなものではありません。ブラウザーのサポートが開始されてから、JavaScriptからその値を操作することが可能になりました。

カスタムプロパティをJavaScriptで利用する方法にはいくつかありますが、 setProperty を利用する方法が挙げられます。

document.documentElement.style.setProperty("--padding", 124 + "px"); // 124px

CSS変数を抽出するには、 getComputedStyle を使います。このロジックの背景はいたってシンプルです。カスタムプロパティはスタイルの一部ですので、算出スタイル (computed style) の一部でもあると言えるのです。

getComputedStyle(document.documentElement).getPropertyValue('--padding') // 124px

同様に getPropertyValue がありますが、これによりHTML上のインラインスタイルの値を抽出することができます。

document.documentElement.style.getPropertyValue("--padding'"); // 124px

カスタムプロパティはスコープ定義となるので、特定の要素から算出スタイル (computed style) を取得する必要があります。
ここでは、前述の setProperty を使って :root に変数を定義しているので値を得ることができています。

Sass変数をJavaScriptで利用する

Sassは事前処理(pre-processing)言語ですので、ウェブサイトの一部として機能するには事前にCSSへの変換が行われます。
そのため、CSSカスタムプロパティと同じようにJavaScriptからアクセスすることはできません。CSSカスタムプロパティは算出スタイル (computed style) としてDOM上でアクセスが可能だからです。

これを変えるためにbuildプロセスを変更する必要があります。とはいえ、多くの場合loaderがbuildプロセスをになってくれるのでそう大きな変更は必要ないと思います。しかし、そうではないプロジェクトの場合は、Sassモジュールのインポート (import) と翻訳 (tranlating) を行う3つのモジュールを利用して以下のように webpack の設定を行います。

module.exports = {
 // ...
 module: {
  rules: [
   {
    test: /\.scss$/,
    use: ["style-loader", "css-loader", "sass-loader"]
   },
   // ...
  ]
 }
};

To make Sass (or, specifically, SCSS in this case) variables available to JavaScript, we need to “export” them.
Sass変数(今回は SCSS)をJavaScriptで利用するには :export する必要があります。

// variables.scss
$primary-color: #fe4e5e;
$background-color: #fefefe;
$padding: 124px;

:export {
  primaryColor: $primary-color;
  backgroundColor: $background-color;
  padding: $padding;
}

この :export はwebpackが変数をimportするための魔法です。何が良いかというと、変数名を変えることができる (camelCase) ことと、利用する変数を選別できることです。

次にこのSassファイル (variables.scss) をJavaScriptでimportします。これで、変数へのアクセスが可能となります。

import variables from './variables.scss';

/*
 {
  primaryColor: "#fe4e5e"
  backgroundColor: "#fefefe"
  padding: "124px"
 }
*/

document.getElementById("app").style.padding = variables.padding;

ただ、この :export にはいくつかの制約があるので述べておいた方が良いと思います。

It must be at the top level but can be anywhere in the file.
- Topレベル階層に位置する必要があるが、ファイル内のどこで定義しても良い
- 同一ファイル内、複数箇所で定義されている場合、キー (key) と値 (value) は結合され一緒にexportされる
- キーの重複がある場合、後続のキーの値が優先される
- 値にはCSSで有効な任意の文字が利用できる(スペースも可)
- 値には引用符(' ")は不要(リテラル文字列として扱われるため)

There are lots of ways having access to Sass variables in JavaScript can come in handy. I tend to reach for this approach for sharing breakpoints. Here is my breakpoints.scs file, which I later import in JavaScript so I can use the matchMedia() method to have consistent breakpoints.

JavaScriptからSass変数にアクセスすることが有用であるケースは多くありますが、筆者の場合、breakpoint を共有するために利用することがよくあります。
以下の breakpoints.scss をJavaScriptでimportし、 matchMedia() で一貫した breakpoint の利用を可能としています。

// Sass variables that define breakpoint values
$breakpoints: (
  mobile: 375px,
  tablet: 768px,
  // etc.
);

// Sass variables for writing out media queries
$media: (
  mobile: '(max-width: #{map-get($breakpoints, mobile)})',
  tablet: '(max-width: #{map-get($breakpoints, tablet)})',
  // etc.
);

// The export module that makes Sass variables accessible in JavaScript
:export {
  breakpointMobile: unquote(map-get($media, mobile));
  breakpointTablet: unquote(map-get($media, tablet));
  // etc.
}

もう一つのアプローチとしてアニメーションがあります。 アニメーションで利用する duration は通常CSSSで保持されますが、複雑なアニメーションになるとJavaScriptが必要となります。

// animation.scss
$global-animation-duration: 300ms;
$global-animation-easing: ease-in-out;

:export {
  animationDuration: strip-unit($global-animation-duration);
  animationEasing: $global-animation-easing;
}

変数 export に strip-unit() を利用していますが、これはJavaScript側でのparse処理を容易にするためです。


CSS、SassとJavaScriptでのデータのやりとりが簡単にできることは嬉しいことで、このような変数の共有はコードをシンプルかつ無駄のないもの(原文にはDRYとあります)にしてくれます。

もちろん、他にもこれを実現するアプローチはあります。Les James氏は2017に興味深いアプローチを紹介しています。彼が紹介しているのは、JSONを利用したデータの共有です。ただ、偏った見方になるかもしれませんが、この記事で紹介したアプローチが最もシンプルで直感的であると思います。すでに運用しているCSSやJavaScriptへの面倒な変更は不要です。

他にもアプローチがあれば、ぜひコメントください。どう解決しているのかを知りたいです!

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

東京都におけるコロナウイルス感染者数推移をグラフ化、都道府県毎の累計感染者数を表示してみた。

閲覧ありがとうございます。

やったこと

コロナウイルスの感染者情報が載っているJsonファイルにアクセスし、東京都の感染者の推移をグラフ化、都道府県毎の累計感染者数を表示してみました。

  1. JavaScriptのfetchメソッドを使ってネット上のJsonファイルにアクセス
  2. 取得したJsonファイルを修正
  3. グラフの表示
  4. 日本地図の表示

全国の感染者数

各都道府県別の感染者数
都道府県をクリックすると、感染者数が出るようになっております。

使ったJsonファイル

全国の感染者情報
各都道府県の感染者情報

使った言語

JavaScript
JQuery

詰まったこと

fetchメソッドで取得した値がchart.jsに上手く反映されない

fetchメソッドで欲しい値は取得したのですが、最初の画面ロード時にchart.jsに反映されませんでした。
なぜか、オプション+コマンド+iを押して検証モードにすると、グラフが上手く表示されるというエラーに出会いました、、、
どうやらfetchメソッドの中にchart.jsでグラフ表示をする処理を書かなければならなかったようです。
How To Make A Chart Using Fetch & REST API's

不正解

    // fetchでJsonファイル取得
    fetch(urlNation)
    .then(function(response) {
        return response.json();
    })
    .then(function(myJson) {                                      
        for(var i = 0; i < myJson.patients_summary.data.length; i++){
            total += myJson.patients_summary.data[i].小計;
            let numPerDay = myJson.patients_summary.data[i].日付.substr(0,10);
            inDate.push(numPerDay);
            inNumber.push(total);
            console.log(myJson.patients_summary.data[i].日付);
        }
        return false;
    });

    // chart.jsでグラフ表示
    var myBarChart = new Chart(ctx, {
      type: 'bar',
      data: {
        labels: inDate,
        datasets: [
          {
            label: 'Infexted Patients',
            data: inNumber,
            backgroundColor: "rgba(21,255,0,0.8)"
          }
        ]
      },
      options: {
        title: {
          display: true,
          text: 'Coronavirus Cases'
        },
        scales: {
          yAxes: [{
            ticks: {
              suggestedMax: 1500,
              suggestedMin: 0,
              stepSize: 100,
              callback: function(value, index, values){
                return  value
              }
            }
          }]
        },
      }
    });

正解

    fetch(urlNation)
    .then(function(response) {
        return response.json();
    })
    .then(function(myJson) {                                      
        for(var i = 0; i < myJson.patients_summary.data.length; i++){
            total += myJson.patients_summary.data[i].小計;
            let numPerDay = myJson.patients_summary.data[i].日付.substr(0,10);
            inDate.push(numPerDay);
            inNumber.push(total);
            console.log(myJson.patients_summary.data[i].日付);
        }
        var myBarChart = new Chart(ctx, {
          type: 'bar',
          data: {
            labels: inDate,
            datasets: [
              {
                label: 'Infexted Patients',
                data: inNumber,
                backgroundColor: "rgba(21,255,0,0.8)"
              }
            ]
          },
          options: {
            title: {
              display: true,
              text: 'Coronavirus Cases'
            },
            scales: {
              yAxes: [{
                ticks: {
                  suggestedMax: 1500,
                  suggestedMin: 0,
                  stepSize: 100,
                  callback: function(value, index, values){
                    return  value
                  }
                }
              }]
            },
          }
        });
        return false;
    });

まとめ

実際に自分で作ってみて、感染者が指数関数的に増えていて、驚いています。
手洗いうがい、十分な睡眠と栄養摂取をして、対策をしましょう!

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

JavaScript の非同期処理とシングルスレッド

本記事の目的

JavaScriptNode.js はよくシングルスレッドだ〜、と言われますが、では非同期処理はどうやって実行されているのか (Non-Blocking I/O) をざっくりと (私の身内に) 説明する為のサンプルコードです。

Node.js, V8 のコードレベルでちゃんと理解したいのであれば、以下のサイトが大変参考になりました。

検証環境

  • iMac (Retina 5K, 27-inch Late 2014), 4 GHz Intel Core i7
  • Node.js v12.13.0
$ nodebrew install-binary v12.13.0
$ nodebrew use v12.13.0

ブラウザ JavaScript の Event loop はまたちょっと違います。

早速サンプルコードから

以下の様な JavaScript index.js を、Node.js で実行します。

  1. 【処理 1】ミリ秒で終わる処理を setTimeout() で 5 秒後に発火.
  2. 【処理 2】ミリ秒で終わる処理を setTimeout() で 0 秒後に発火.
  3. 【処理 3】10 秒かかる同期処理を実行.
  • 時間の計測には Node.js 標準 API の perf_hooks モジュールを使用しています。 Node.js プロセス実行開始からのミリ秒を得られます
  • コード中では、ミリ秒 → 秒、に変換して表示しています
index.js
const { performance } = require('perf_hooks');

/**
 * @return 本スクリプトを実行してからの経過秒数.
 */
const seconds = () => performance.now() / 1000;
const secondsPadded = () => seconds().toFixed(6).padStart(10, ' ');  // 長さ揃える.

//////////////// 処理3つ ////////////////

/**
 * 処理 1 (非同期, 5 秒後に発火).
 */
const func1 = () => {
  console.log(`${secondsPadded()} seconds --> 処理 1 (非同期, 5 秒後に発火)`);
};

/**
 * 処理 2 (非同期, 0 秒後に発火).
 */
const func2 = () => {
  console.log(`${secondsPadded()} seconds --> 処理 2 (非同期, 0 秒後に発火)`);
};

/**
 * 処理 3 (同期. 10 秒かかる).
 */
const func3 = () => {
  while (seconds() < 10) { /* consuming a single cpu for 10 seconds... */ }

  console.log(`${secondsPadded()} seconds --> 処理 3 (同期, 10 秒かかる)`);
};

//////////////// 計測開始 ////////////////

console.log(`${secondsPadded()} seconds --> index.js START`);

// [非同期] 5 秒後に実行.
setTimeout(func1, 5000);

// [非同期] 即時実行.
setTimeout(func2);

// 同期実行.
func3();

console.log(`${secondsPadded()} seconds --> index.js END`);

//////////////// 計測終了 ////////////////

期待値?

なんとなく 「こう動作するだろう...」 という気分になるのは ↓ でしょう。

$ node index.js

  0.000000 seconds --> index.js START
  0.000000 seconds --> 処理 2 (非同期, 0 秒後に発火)
  5.000000 seconds --> 処理 1 (非同期, 5 秒後に発火)
 10.000000 seconds --> 処理 3 (同期, 10 秒かかる)
 10.000000 seconds --> index.js END

実際は...

現実はこうです。何故でしょうか。

$ node index.js 

  0.175104 seconds --> index.js START
 10.000085 seconds --> 処理 3 (同期, 10 秒かかる)
 10.000210 seconds --> index.js END
 10.000955 seconds --> 処理 2 (非同期, 0 秒後に発火)
 10.001161 seconds --> 処理 1 (非同期, 5 秒後に発火)

シングルスレッドだから、順番に処理している

おおよそ、Node.js の内部では ↓ のように処理がシングルスレッドで行われています。

  1. JavaScript コンテキストの生成時にイベントループが生成されます
  2. 最初のエントリ JavaScript index.js がタスクとして、未実行キューに乗ります
  3. イベントループ
    1. 未実行キューから index.js タスクが取り出され、実行が開始されます
      1. setTimeout(処理1, 5秒) が実行され、【処理 1】がタイマーキューに追加されます
      2. setTimeout(処理2, 0秒) が実行され、【処理 2】がタイマーキューに追加されます
      3. 【処理 3】が同期的に実行され、10 秒間、CPU (シングルコア) を専有します
    2. index.js タスクの実行が終了します
  4. イベントループ
    1. タイマーキューから 有効期限が切れたタスク【処理 2】 を取り出し、実行が開始されます
    2. 【処理 2】タスクの実行が終了します
  5. イベントループ
    1. タイマーキューから 有効期限が切れたタスク【処理 1】 を取り出し、実行が開始されます
    2. 【処理 1】タスクの実行が終了します

実際はタイマー Phase はキューではない (FIFO でもない) ですが、説明の都合上そう表記しました。

JavaScript Non-Blocking I_O Architecture.png

要はイベントループにて、実行可能なタスクがあれば即時実行し、なければ I/O 待ち (epoll) をすることになります。

結論

つまり、setTimeout() 等の非同期タイマー処理は...

  • 指定した時間が来たら即座に Callback を実行する. (OS 割り込みみたいに)

ではなく...

  • 指定した時間を 過ぎてたら Callback を できるだけ早く 実行する

ですね。

それは Promise や、Network Socket I/O 待ちである fetch でも同じで...

  • Callback が実行可能になってから (現在実行中の他の処理を待って) 順番が来たら (やっと) 実行開始する

です。

参考文献

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

JavaScript (Node.js) の非同期処理とシングルスレッド

本記事の目的

JavaScriptNode.js はよくシングルスレッドだ〜、と言われますが、では非同期処理はどうやって実行されているのか (Non-Blocking I/O) をざっくりと (私の身内に) 説明する為のサンプルコードです。

Node.js, V8 のコードレベルでちゃんと理解したいのであれば、以下のサイトが大変参考になりました。

検証環境

  • iMac (Retina 5K, 27-inch Late 2014), 4 GHz Intel Core i7
  • Node.js v12.13.0
$ nodebrew install-binary v12.13.0
$ nodebrew use v12.13.0

ブラウザ JavaScript の Event loop はまたちょっと違います。

早速サンプルコードから

以下の様な JavaScript index.js を、Node.js で実行します。

  1. 【処理 1】ミリ秒で終わる処理を setTimeout() で 5 秒後に発火.
  2. 【処理 2】ミリ秒で終わる処理を setTimeout() で 0 秒後に発火.
  3. 【処理 3】10 秒かかる同期処理を実行.
  • 時間の計測には Node.js 標準 API の perf_hooks モジュールを使用しています。 Node.js プロセス実行開始からのミリ秒を得られます
  • コード中では、ミリ秒 → 秒、に変換して表示しています
index.js
const { performance } = require('perf_hooks');

/**
 * @return 本スクリプトを実行してからの経過秒数.
 */
const seconds = () => performance.now() / 1000;
const secondsPadded = () => seconds().toFixed(6).padStart(10, ' ');  // 長さ揃える.

//////////////// 処理3つ ////////////////

/**
 * 処理 1 (非同期, 5 秒後に発火).
 */
const func1 = () => {
  console.log(`${secondsPadded()} seconds --> 処理 1 (非同期, 5 秒後に発火)`);
};

/**
 * 処理 2 (非同期, 0 秒後に発火).
 */
const func2 = () => {
  console.log(`${secondsPadded()} seconds --> 処理 2 (非同期, 0 秒後に発火)`);
};

/**
 * 処理 3 (同期. 10 秒かかる).
 */
const func3 = () => {
  while (seconds() < 10) { /* consuming a single cpu for 10 seconds... */ }

  console.log(`${secondsPadded()} seconds --> 処理 3 (同期, 10 秒かかる)`);
};

//////////////// 計測開始 ////////////////

console.log(`${secondsPadded()} seconds --> index.js START`);

// [非同期] 5 秒後に実行.
setTimeout(func1, 5000);

// [非同期] 即時実行.
setTimeout(func2);

// 同期実行.
func3();

console.log(`${secondsPadded()} seconds --> index.js END`);

//////////////// 計測終了 ////////////////

期待値?

なんとなく 「こう動作するだろう...」 という気分になるのは ↓ でしょう。

$ node index.js

  0.000000 seconds --> index.js START
  0.000000 seconds --> 処理 2 (非同期, 0 秒後に発火)
  5.000000 seconds --> 処理 1 (非同期, 5 秒後に発火)
 10.000000 seconds --> 処理 3 (同期, 10 秒かかる)
 10.000000 seconds --> index.js END

実際は...

現実はこうです。何故でしょうか。

$ node index.js 

  0.175104 seconds --> index.js START
 10.000085 seconds --> 処理 3 (同期, 10 秒かかる)
 10.000210 seconds --> index.js END
 10.000955 seconds --> 処理 2 (非同期, 0 秒後に発火)
 10.001161 seconds --> 処理 1 (非同期, 5 秒後に発火)

シングルスレッドだから、順番に処理している

おおよそ、Node.js の内部では ↓ のように処理がシングルスレッドで行われています。

  1. JavaScript コンテキストの生成時にイベントループが生成されます
  2. 最初のエントリ JavaScript index.js がタスクとして、未実行キューに乗ります
  3. イベントループ
    1. 未実行キューから index.js タスクが取り出され、実行が開始されます
      1. setTimeout(処理1, 5秒) が実行され、【処理 1】がタイマーキューに追加されます
      2. setTimeout(処理2, 0秒) が実行され、【処理 2】がタイマーキューに追加されます
      3. 【処理 3】が同期的に実行され、10 秒間、CPU (シングルコア) を専有します
    2. index.js タスクの実行が終了します
  4. イベントループ
    1. タイマーキューから 有効期限が切れたタスク【処理 2】 を取り出し、実行が開始されます
    2. 【処理 2】タスクの実行が終了します
  5. イベントループ
    1. タイマーキューから 有効期限が切れたタスク【処理 1】 を取り出し、実行が開始されます
    2. 【処理 1】タスクの実行が終了します

実際はタイマー Phase はキューではない (FIFO でもない) ですが、説明の都合上そう表記しました。

JavaScript Non-Blocking I_O Architecture.png

要はイベントループにて、実行可能なタスクがあれば即時実行し、なければ I/O 待ち (epoll) をすることになります。

結論

つまり、setTimeout() 等の非同期タイマー処理は...

  • 指定した時間が来たら即座に Callback を実行する. (OS 割り込みみたいに)

ではなく...

  • 指定した時間を 過ぎてたら Callback を できるだけ早く 実行する

ですね。

それは Promise や、Network Socket I/O 待ちである fetch でも同じで...

  • Callback が実行可能になってから (現在実行中の他の処理を待って) 順番が来たら (やっと) 実行開始する

です。

参考文献

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

Chart.jsで遊んでみた

折れ線グラフも棒グラフも自由自在!

chart.html
<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script>
  </head>

  <body>
    <canvas id="myChart"></canvas>
    <script>
        var ctx = document.getElementById('myChart').getContext('2d');
        var chart = new Chart(ctx, {
            // The type of chart we want to create
            type: 'horizontalBar', //bar 棒グラフ、line 折れ線グラフ、horizontalBar 横の棒グラフ

            // The data for our dataset
            data: {
                labels: ['January', 'February', 'March', 'April', 'May', 'June'],
                datasets: [{
                    label: '@NJ',
                    data: [80, 50, 60, 40, 30, 150],
                    backgroundColor: 'skyblue',
                    borderColor: 'blue',
                    borderWidth: 0,
                    fill: false,
                    pointStyle: 'rect'
                },{
                    label: '@haruka',
                    data: [100, 100, 40, 50, 30, 100],
                    // borderColor: 'blue',
                    // borderWidth: 5,
                    backgroundColor: [
                      'hsla(90, 60%, 60%, 0.3)',
                      'hsla(180, 60%, 60%, 0.3)',
                      'hsla(270, 60%, 60%, 0.3)',
                      'hsla(360, 60%, 60%, 0.3)',
                      'hsla(0, 60%, 60%, 0.3)',
                      'hsla(80, 60%, 60%, 0.3)',
                    ],
                    lineTension: 0,
                    pointStyle: 'triangle'
                }]
            },

            // Configuration options go here
            options: {
              // scales: {
              //   yAxes: [{
              //     ticks: {
              //       // min: 0,
              //       // max:100
              //       suggestedMin: 0,
              //       suggestedMax: 100,
              //       stepSize: 10,
              //       callback: function(value, index, values){
              //         return 'JPY'+ value;
              //         }
              //     }
              //   }]
              // },

              //積み上げ棒グラフ
              scales:{
                xAxes: [{
                  stacked: true
                }],
                yAxes: [{
                  stacked: true
                }]
              },
              title: {
                display: true,
                text: 'Annual Sales',
                fontSize: 18,
                position:'left'
              },
              animation: {
                duration: 0
              },
              legend:{
                // position: 'right'
                // display: false
              }
            }
        });
        var myLineChart = new Chart(ctx, {
            type: type,
            data: data,
            options: options
        });
        </script>
  </body>
</html>

結果は以下の図の通り。最初は折れ線グラフで作ったり枠の色変えたり
縦の棒グラフ作ってたりしたから不要なコードも沢山あります。
(//でコメントにしてるよ)
image.png

折れ線グラフ

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>my Chart</title>
</head>
<body>
  <canvas id ="my_chart">
    Canvas not supported...
  </canvas>
  <script
  src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script>

  <script>
      'use strict';

      var type = 'line';

      var data = {
        labels: [2010, 2011, 2012, 2013],
        datasets: [{
          label: '@haruka',
          data: [120, 130, 140, 150]
        }, {
          label: '@koji',
          data: [180, 200, 150, 300]
        }]
      };
      var options; 
      var ctx = document.getElementById('my_chart').getContext('2d');
      var myChart = new Chart(ctx, {
        type: type,
        data: data,
        options: options
      });
  </script>
</body>
</html> 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リモート勤務状況を Slack に常に反映する

はじめに

リモートワーク、最近は、テレワーク、自宅勤務、在宅勤務、様々な呼び方をすることが多いですね。世界中を席巻する例の細菌感染とともに、かつてない流行りをみせています。あなたの会社ではどうですか?

そもそもは、専門職なんかは特に顕著ですが、職場に行かなくたって上手く回る仕事は世の中にありふれているし、そうした可能性を否定するいくつものつまらない主張は、きっかけさえあればこんな風にサクッと是正されるものかもしれません。誰が毎朝毎夕満員電車で汗だくで他人とおしくらまんじゅうしたいです?

これまで、リモートワークというものは、多くの人たちの固定観念ともいえるその職場主義によって、ほとんどの組織で導入を妨げられてきた歴史があります。そんな練度の低さでは、おいそれと使いこなせるわけはないのです。上手くいかないのが正常です。相応のナレッジが必要です。私も実戦経験が少なく苦戦する日々です。それをまず理解したいところです。

ということで前置きが長くなってしまいましたが、まぁだいたいのエンジニアが所属する組織では、コミュニケーションの中心に Slack のようなテキストチャットメインのコラボレーションツールを使っていると思います。リモートワークを導入するにあたり、メンバーの勤務状況をツールを通して把握することはチームにとって重要です。今回は、無料でこの仕組みの自動化を目指します。

最新版のソースコードはリポジトリをご覧ください。

構成

Slack API トークン取得

非推奨のレガシートークンを使う手もありますが、ちゃんと OAuth トークンの取得方法を載せておきます。

↓アプリを作成します。

↓ワークスペースに対するアクセス権限を定義します。
users.profiles:write というユーザープロフィールを変更できる権限のみをユーザートークンに設定します。
これでトークンが悪意のある第三者に知られても、あなたのプロフィールを変更することしかできません。

↓インストールします。

↓権限を許可すると、トークンが生成されます。

Google Apps Script 作成

↓Spreadsheet に紐付くスクリプトを作成します。
Spreadsheet は、IFTTT Webhook で受信したデータのストレージとして機能します。

↓スクリプトエディタを開きます。

スクリプトをコピペし、Slack トークンをはじめとした必要な情報を埋めます。

↓IFTTT の Webhook となる Web App をデプロイします。

↓Execute the app as: Me (自分が実行), Who has access to the app: Anyone, even anonymous (誰でもアクセス可) となっていることに注意してください。

↓Web App の次は、トリガーを追加します。

↓実行する関数は update です。また Spreadsheet の内容をポーリングすることになるので、実行頻度は短めにします。

↓ちなみにスクリプトの実行ログはここで見れます。

Google Calendar 予定を追加

在宅勤務を想定する以上、特定の場所にいるだけでステータスを決定するわけにはいきません。寝ている間もずっと働いてることになってしまいます。カレンダーの予定も考慮することにしましょう。

↓このような形で、その日はどこで働くのかわかるように、場所をタイトルに含めた予定をまとめてドカンと追加しましょう。画像では、休暇を入れながら、原則として月曜だけ出社することにしています。

IFTTT レシピ作成

在宅勤務、職場勤務、この 2 つの場所のレシピを作成します。今回は 2 つだけですが、場所の数だけレシピは必要です。

↓まずは、在宅勤務レシピの This を作成していきます。

↓This は、Location サービスを設定します。

↓入出の情報が欲しいので、トリガーは左下の "You enter or exit an area" にします。

↓あなたの家の場所を範囲指定します。範囲が広すぎても狭すぎてもよくないので、ちょうどいい感じに調整します。

↓今度は That です。

↓That は Webhooks にします。入出のタイミングで Web App を実行してもらうためです。

Location で設定した位置範囲に来た時、Google Apps Script の Web App に、entered または exited という文字列が POST されます。
それぞれ、範囲への入と出を表します。
URL は Web App をデプロイした際に取得できる URL の末尾に ?place={場所名}
を追加しておきます。場所名は、レシピによって異なるので以下を参考に設定してください。

  • 在宅勤務レシピ URL に追加する文字列: ?place=%E5%9C%A8%E5%AE%85
  • 職場勤務レシピ URL に追加する文字列: ?place=%E8%81%B7%E5%A0%B4

JavaScript の実行環境があれば、以下の要領で場所名を作成できます。

encodeURIComponent('在宅') == '%E5%9C%A8%E5%AE%85'
encodeURIComponent('職場') == '%E8%81%B7%E5%A0%B4'

↓レシピの名前を設定して完了です。

↓Connected になっていることを確認します。

同じ要領で「職場勤務」レシピも作成したら完成です!

さいごに

Unlicense だから自由にフォークしてね。特化するも汎化するも応用するも良しです。

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

【JavaScript】月初・月末取得や日数0付けや日付比較

概要

JavaScriptのDateオブジェクトを使用している中で、よく忘れてしまう事を備忘録として残します。

月初の取得

 function getFirstDate (date) {
   return new Date(date.getFullYear(), date.getMonth(), 1);
 }
 var date = getFirstDate(new Date());
 console.log(date); // Wed Apr 01 2020 00:00:00 GMT+0900 (日本標準時)

月末の取得

 function getLastDate (date) {
   return new Date(date.getFullYear(), date.getMonth() + 1, 0);
 }
 var date = getLastDate(new Date());
 console.log(date); // Thu Apr 30 2020 00:00:00 GMT+0900 (日本標準時)

日付や月の前に0を付ける

 var today = new Date();
 var date = ("0"+today.getDate()).slice(-2);
 var month = ("0"+ (today.getMonth()+1)).slice(-2); 
 console.log(`${today.getFullYear()}-${month}-${date}`) // 2020-04-04  

日付の比較

 var date1 = new Date();
 var date2 = new Date("2020-03-01");
 console.log(date1.getTime() > date2.getTime()) // true
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Concurrent Mode時代のReact設計論 (4) コンポーネント設計にサスペンドを組み込む

この記事は「Concurrent Mode時代のReact設計論」シリーズの4番目の記事です。

シリーズ一覧

コンポーネント設計にサスペンドを組み込む

前回の最後にrender-as-you-fetchという概念が出てきました。これは、ReactのConcurrent Modeのドキュメントにおいて提唱されているUXパターンであり、読み込んで表示すべきデータが複数ある場合に、全てが読み込み完了するまで待つのではなく読み込めたデータから順に表示するというものです。

このパターンの良し悪しはともかく、これはConcurrent Mode時代のコンポーネント設計を議論するための格好の題材です。

基本パターン: データごとにPromiseを分ける

Concurrent Modeにおいてrender-as-you-fetchを実現するには、それぞれのデータに対して異なるPromise(Fetcher)を用意する必要があります。そして、各データを担当するコンポーネントを用意して、それぞれのコンポーネントがサスペンドします。

そうすることで、それぞれのデータが用意できた段階でコンポーネントのサスペンドが解除(再レンダリング)され、その部分のデータが表示されます。

具体例として、ユーザーのリストを3種類読み込んでrender-as-you-fetch戦略で表示するコンポーネントを書いてみるとこんな感じです。

const PageB: FunctionComponent<{
  dailyRankingFetcher: Fetcher<User[]>;
  weeklyRankingFetcher: Fetcher<User[]>;
  monthlyRankingFetcher: Fetcher<User[]>;
}> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => {
  return (
    <>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={dailyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={weeklyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={monthlyRankingFetcher} />
      </Suspense>
    </>
  );
};

const Users: FunctionComponent<{
  usersFetcher: Fetcher<User[]>;
}> = ({ usersFetcher }) => {
  const users = usersFetcher.get();
  return (
    <ul>
      {users.map(({ id, name }) => (
        <li key={id}>{name}</li>
      ))}
    </ul>
  );
};

PageBは3種類のFetcher<User[]>を受け取ります。実際にFetcher<User[]>から得てデータを表示するのは別に用意したUsersコンポーネントが担当しており、PageBの役割は各Users要素をSuspenseで囲むことです。

ポイントは、このようにUsersをそれぞれSuspenseで囲まないといけないということです。復習すると、Suspenseの役割はその内部で発生したサスペンドをキャッチして、その場合にfallbackで指定されたフォールバックコンテンツを代わりにレンダリングすることです。Suspenseの中のどこでサスペンドが発生しようと、そのSuspenseの中身全体がフォールバックします。

このことから、Suspenseを用いてrender-as-you-fetchパターンを実装するには、あるコンポーネントがサスペンドしても他の部分に影響を与えないようにする必要があります。ここではSuspenseを複数並べることでこれを達成しています。

実際このPageBを適当なデータでレンダリングすると、下のスクリーンショットのように一つずつLoading users...Usersによってレンダリングされたデータに置き換わっていく挙動をとります。

screenshot4-1.png

ちなみに、Suspenseの組み立て方によって色々な表示パターンを実現することができます。例えば、次のようにすると、dailyRankingFetcherが用意できるまでは何も表示せず、用意できたら残りを待つという挙動になります。

      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={dailyRankingFetcher} />
        <Suspense fallback={<p>Loading users...</p>}>
          <Users usersFetcher={weeklyRankingFetcher} />
          <Suspense fallback={<p>Loading users...</p>}>
            <Users usersFetcher={monthlyRankingFetcher} />
          </Suspense>
        </Suspense>
      </Suspense>

このように、コンポーネントが非同期処理の結果をどのように待ってどう表示するのかというロジックはSuspenseを用いて書くことができます。これはつまり、Concurrent Modeではrender-as-you-fetchパターンに必要なロジックを実際にそのデータを表示するコンポーネントが(内部でのコンポーネント分割は起こりますが)記述できるということを表しています。前回の記事で示した問題の一つがConcurrent Modeでは解決されているわけです。

おまけに、Suspenseという道具を用いて、データローディングに係るロジックをJSXというきわめて宣言的な表現により記述することができています。一応誤解がないように述べておくとJSXという構文は重要ではなく、本質的にこの点に寄与しているのはコンポーネントが成す木構造という表現方法なのですが。

なお、この立場に立つと、レンダリングのサスペンドというのはコンポーネントが発生させる現象ですから、サスペンドする役割を持つコンポーネント(今回はUsers)を明確にすることが重要になります。コンポーネントにdoc commentなどを書く際に、「このコンポーネントはいつサスペンドするのか」を明示しておくのもよいでしょう。

useTransitionSuspenseの関係

まずuseTransitionについて復習します。このフックからはstartTransition関数を得ることができ、startTransitionの内部で発生したステート更新により再レンダリングが発生してそのレンダリングでサスペンドが発生した場合、サスペンドが解消されるまで画面に更新前のステートを表示し続けられるというものでした。

useTransitionが絡むと、Suspenseに係るコンポーネント設計はかなり複雑な様相をとります。これに関連して、ひとつ重要な事実を覚えていただく必要があります。

それは、再レンダリング時に新たにマウントされたSuspenseの中で起きたサスペンドはuseTransitionからは無視されるという点です。言い方を変えれば、useTransitionの効果を発動するには、あらかじめ用意してあったSuspenceにサスペンドをキャッチしてもらう必要があるということです。

この挙動はバグなのではと筆者は一瞬思いましたが、このissueで説明されている通りこれは仕様です。

コンポーネントを設計する際にはこのことを念頭に考える必要があります。すなわち、あるコンポーネントの中でサスペンドを発生させるにあたり、それがuseTransitionに対応するサスペンド(外部のSuspenseによりキャッチされることを意図したサスペンド)なのか、それともuseTransitionに対応しないサスペンド(自身の中で新たに生成したSuspenseによりキャッチされるサスペンド)なのかを意識的に区別しなければならないということです。

では、先ほど出てきたPageBの場合はどうでしょうか。

const PageB: FunctionComponent<{
  dailyRankingFetcher: Fetcher<User[]>;
  weeklyRankingFetcher: Fetcher<User[]>;
  monthlyRankingFetcher: Fetcher<User[]>;
}> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => {
  return (
    <>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={dailyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={weeklyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={monthlyRankingFetcher} />
      </Suspense>
    </>
  );
};

startTransition中のステート更新で新たにPageBコンポーネントがマウントされた場合、3つのSuspenseコンポーネントがマウントされ、その中で発生したサスペンドは即座にキャッチされます。このとき、ステート更新によって発生したサスペンドは全て新たにマウントされたSuspenseによってキャッチされることになります。

よって、このステート更新ではuseTransitionの効果は発揮されません。ステート更新が行われた瞬間にPageBがレンダリングされDOMに反映されます。PageBは最初3つのLoading users...を表示することになるでしょう。実際、このPageBに前回の記事で出てきたRootPageAを繋げてみるとそのような挙動になります。興味がある方は実際にやってみましょう。

useTransitionのための応用的なコンポーネントデザイン

このことを踏まえて、PageBuseTransitionに対応するように改良するにはどうすればよいか考えてみましょう。もし「全部ロードされるまで前の画面を表示し続けたい」という場合は話は簡単で、それぞれのUsersコンポーネントをSuspenseで囲むのをやめればよいです。また、例えば「dailyRankingFetcherがロード完了するまでは前の画面を表示し続けたい」のような場合も、対応するUsersだけSuspenseで囲まなければ対応できます。

厄介なのは、「どれか1つのデータが読み込めるまでは前の画面を表示し続けたい」というような場合です。この場合はただSuspenseを消すだけでは達成できません。

Promiseの機能を思い出すと、「どれか1つのPromiseが解決するまで待つ」という挙動はPromise.raceにより達成できます。ということで、今回はFetcher.raceを用意すれば解決できますね。

Fetcher.raceの実装を用意するとこんな感じです(コンストラクタをあのインターフェースにしたので実装がひどいことになっていますがサンプルだと思って大目に見てください)。

  static race<T extends Fetcher<any>[]>(
    fetchers: T
  ): Fetcher<FetcherValue<T[number]>> {
    for (const f of fetchers) {
      if (f.state.state === "fulfilled") {
        const result = new Fetcher<any>(() => Promise.resolve());
        result.state = {
          state: "fulfilled",
          value: f.state.value
        };
        return result;
      } else if (f.state.state === "rejected") {
        const result = new Fetcher<any>(() => Promise.resolve());
        result.state = {
          state: "rejected",
          error: f.state.error
        };
      }
    }
    return new Fetcher(() =>
      Promise.race(fetchers.map(f => (f as any).promise))
    );
  }

ちなみに、型に出てきたFetcherValueはこのように定義しています。型安全な実装が厳しい場合でも、型パズルでも何でも駆使して関数のインターフェースだけは正確さを守るというのが堅牢なTypeScriptプログラムを書くコツです。

type FetcherValue<F> = F extends Fetcher<infer T> ? T : unknown;

話を元に戻すと、このFetcher.raceを使ってPageBをこのように定義すれば、「どれか1つのデータが来るまでサスペンドする」という挙動が実現できます。Fetcher.race([...])getメソッドを使用するためだけに作られており、その値はPageB直下では使われていません。このように、値を得ることではなくサスペンドすることが主目的のFetcherというのも存在し得ます。少し話が違いますが、筆者も第1回の記事で紹介したアプリケーションではFetcher<void>を多用しています。

const PageB: FunctionComponent<{
  dailyRankingFetcher: Fetcher<User[]>;
  weeklyRankingFetcher: Fetcher<User[]>;
  monthlyRankingFetcher: Fetcher<User[]>;
}> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => {
  Fetcher.race([
    dailyRankingFetcher,
    weeklyRankingFetcher,
    monthlyRankingFetcher
  ]).get();

  return (
    <>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={dailyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={weeklyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={monthlyRankingFetcher} />
      </Suspense>
    </>
  );
};

再レンダリング時のサスペンド設計

ここからは、一旦最初のPageBに頭を戻して考えます。

const PageB: FunctionComponent<{
  dailyRankingFetcher: Fetcher<User[]>;
  weeklyRankingFetcher: Fetcher<User[]>;
  monthlyRankingFetcher: Fetcher<User[]>;
}> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => {
  return (
    <>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={dailyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={weeklyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={monthlyRankingFetcher} />
      </Suspense>
    </>
  );
};

これまでの議論ではPageBが新規にマウントされた場合を考えていました。このときは中身のSuspenseが新規にマウントされるので、その中のUsersがサスペンドしてもuseTransitionが反応しないのでした。

では、PageBがすでにマウントされている状態で、startTransitionの中のステート更新に起因してPageBのpropsが変わった場合はどうでしょうか。新しくpropsから渡されたFetcherによってサスペンドした場合、それをキャッチするのはPageBがレンダリングしたSuspenseであることに変わりませんが、今回はこれらのSuspenseはあらかじめマウントしてあったSuspenseです。なぜなら、前回のPageBのレンダリングによってこのSuspenseはすでにマウントされていたからです。よって、この場合はuseTransitionが働きます。

つまり、PageBは「新しくマウントされたときはuseTransitionに非対応だが、マウント済の状態でpropsが更新された時はuseTransitionに対応」という特徴を持つコンポーネントなのです。Concurrent Modeによほど精通していなければ、コンポーネントの定義を一目見てこのことを見抜くのは難しいでしょう。

この状態はなんだか一貫性がありませんね。これでも良いならばこの実装で問題ありませんが、どちらかに統一したいということもあるでしょう。まず、常にuseTransitionに対応にしたい場合の方法は先ほどまで述べた通りで、Suspenseを消すなり、Suspenseの中ではなくPageB自体がサスペンドするなりといった方法があります。

一方、常にuseTransition非対応にしたい場合はどうすれば良いでしょうか。答えは、「propsが変わるたびにSuspenseをマウントし直す」です。そうすることでSuspenseは常に新しくマウントされた扱いとなり、その中でのサスペンドはuseTransitionに影響しなくなります。Suspenseをマウントし直すにはkeyを用います。Reactでは、同じコンポーネントでも異なるkeyが与えられた場合は別のコンポーネントと見なされますから、propsが変わるたびにSuspenseに与えるkeyを変えることで、Suspenseをアンマウント→マウントさせることができます。

具体的な方法の一例としては、まず次のようなuseObjectIdカスタムフックを用意します。

export const useObjectId = () => {
  const nextId = useRef(0);
  const mapRef = useRef<WeakMap<object, number>>();

  return (obj: object) => {
    const map = mapRef.current || (mapRef.current = new WeakMap());
    const objId = map.get(obj);
    if (objId === undefined) {
      map.set(obj, nextId.current);
      return nextId.current++;
    }
    return objId;
  };
};

このフックはPageBの中で次のように使います。今回のコードではそれぞれのSuspensekeyが与えられており、keyを計算するためにuseObjectIdを使用しています。

const PageB: FunctionComponent<{
  dailyRankingFetcher: Fetcher<User[]>;
  weeklyRankingFetcher: Fetcher<User[]>;
  monthlyRankingFetcher: Fetcher<User[]>;
}> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => {
  const getObjectId = useObjectId();
  return (
    <>
      <Suspense
        key={`${getObjectId(dailyRankingFetcher)}-1`}
        fallback={<p>Loading users...</p>}
      >
        <Users usersFetcher={dailyRankingFetcher} />
      </Suspense>
      <Suspense
        key={`${getObjectId(weeklyRankingFetcher)}-2`}
        fallback={<p>Loading users...</p>}
      >
        <Users usersFetcher={weeklyRankingFetcher} />
      </Suspense>
      <Suspense
        key={`${getObjectId(monthlyRankingFetcher)}-3`}
        fallback={<p>Loading users...</p>}
      >
        <Users usersFetcher={monthlyRankingFetcher} />
      </Suspense>
    </>
  );
};

useObjectIdフックは関数getObjectIdを返します。この関数は各オブジェクトに対して異なるIDを返します。同じオブジェクトに対しては何回呼んでも同じIDが返されます。これをkeyに組み込むことによって、daylyRankingFetcherなどに別のFetcherが渡されたタイミングでSuspenseに渡されるkeyも更新され、新たなSuspenseがマウントされた扱いになります。

この実装により、PageBが別のpropsで再レンダリングされた場合でも、内部で発生するサスペンドの影響を内部に封じ込めてuseTransitionに影響させないことが可能になりました。

SuspenseuseTransitionの関係を整理する

ここまでは、SuspenseuseTransitionの関係を解説し、ユースケースに合わせた実装法を紹介してきました。なんだか場当たり的な印象を受けた読者の方も多いと思いますので、もう少し整理して見直してみましょう。

あるコンポーネントがPromise(をラップするFetcher)を受け取るとします。そのコンポーネントの責務がそのデータを表示することであれば、必然的にそのコンポーネントはサスペンドを発生させることになります。

コンポーネント内で発生しうるサスペンドは3種類に分類できます。3種類のサスペンドは、「コンポーネントの外にサスペンドが出て行くかどうか」と「useTransitionに対応するかどうか」に注目すると次の表のようにそれぞれ異なる特徴を持ちます。

サスペンドの種類 外に出て行くか useTransition対応
1 内部のSuspenseでキャッチされないサスペンド Yes Yes1
2 内部で新規にマウントされたSuspenseにキャッチされるサスペンド No No
3 内部の既存のSuspenseにキャッチされるサスペンド No Yes

パターン1が一番スタンダートなサスペンドでしょう。あるコンポーネントがFetcherから得たデータを表示することが責務ならば、データがまだない場合にそのコンポーネントがサスペンドするのは自然なことです。

パターン2は逆にサスペンドを完全に内部で抑え込むパターンです。サスペンドが発生しても、そのことはコンポーネントの外部には検知されません。データがまだ無いときの挙動を完全にコンポーネント内で制御したい場合に適しています。

パターン3は、コンポーネントが新規にマウントされた場合は発生せず、再レンダリングのときのみ可能な選択肢です。これは扱うのがやや難しいですが、コンポーネントの内部でuseTransitionを使いたい場合などはこれが一番自然な選択肢となることが多いでしょう。

コンポーネントのロジックを実装する際には、これらを組み合わせることもあるでしょう。例えば、先ほど出てきたFetcher.raceの例は1と2の合わせ技です。

コンポーネントの使い勝手という観点からは、パターン1が最も有利です。パターン1はコンポーネントの外側にSuspenseを配置すれば2や3に変換できますが、逆に2や3を1に変換することはできないからです。

パターン1と2や3の使い分けはコンポーネントの責務に応じて決めるのが良いでしょう。具体的には、データがない場合にフォールバックを表示するという責務をコンポーネントが持っているのであれば、2か3を選択することになります。逆に、その責務を持たずデータがない場合はサスペンドすべきならば、1を選択しなければなりません。

誰がFetcherを用意するのか

Concurrent Modeにおいては、誰かいつ非同期処理を開始する(Fetcherを用意する)のかがとても重要です。従来の基本的なパターンは、データを表示する責務を持ったコンポーネントがuseEffectの中で非同期処理を開始するというものです。Fetcherと組み合わせればこのような実装になるでしょう。

const PageB: FunctionComponent = () => {
  const [dailyRankingFetcher, setDailyRankingFetcher] = useState<
    Fetcher<User[]> | undefined
  >(undefined);
  useEffect(() => {
    setDailyRankingFetcher(new Fetcher(() => fetchUsers()));
  }, []);

  return dailyRankingFetcher !== undefined ? (
    <Users usersFetcher={dailyRankingFetcher} />
  ) : null;
};

しかし、2つの理由からこの実装は忌避すべきです。一つ目の理由は、一度レンダリングされたあとuseEffect内ですぐに再度レンダリングを発生させていることです。これはReactにおける典型的なアンチパターンの一つです。

もう一つの理由は、こうするとPageBが自動的にuseTransitionに非対応になるからです。PageBが最初にレンダリングされたときはまだサスペンドが発生しませんから、PageBに遷移するきっかけとなったステート更新ではサスペンドが発生しなかったことになります。もしPageBに遷移するときにuseTransitionを使いたければ、このような実装は必然的に選択肢から除外されます。

では、どうすればよいのでしょうか。大きく分けて2つの選択肢があります。基本的には、これまでやってきたように外からFetcherを渡すことになります。これについては次回の記事で詳しく扱います。

もう一つ、useEffectの中ではなく最初のレンダリング中に直にFetcherを用意するという戦略を思いついた方もいるかもしれません。しかし、ほとんどの場合これは無理筋です。

useStateFetcherを用意することはできない

例えば、次のような実装を試してみましょう。useStateは関数を渡すと最初のレンダリング時にその関数が呼び出されてステートの初期化に用いられます。次のようにすることでdailyRankingFetcherをいきなりFetcherで初期化し、初手でサスペンドを発生させることができます。

const PageB: FunctionComponent = () => {
  const [dailyRankingFetcher] = useState(() => new Fetcher(() => fetchUsers()));

  return <Users usersFetcher={dailyRankingFetcher} />;
};

しかし、これは期待通りに動きません。PageBはずっとサスペンドしたままになります。その理由は、PageBがレンダリングされるたびに新しいFetcherインスタンスが生成されるからです。

PageBが最初にレンダリングされた場合はuseStateに渡された関数が呼ばれて新しいFetcherインスタンスがdailyRankingFetcherに入ります。ここまでは想定通りですが、その後サスペンド明けにPageBが再度レンダリングされたとき、PageBは初回レンダリングという扱いになります。よって、dailyRankingFetcherに入るのはまた新しく作られたFetcherインスタンスとなり、PageBは再度サスペンドします。これを繰り返すことになり、PageBは永遠に内容をレンダリングすることができません。

すなわち、レンダリングの結果サスペンドが発生したときはレンダリングが完了したと見なされず、useStateフックなどの内容はセーブされません。あたかも、そのレンダリングが無かったかのように扱われます。useMemoなども同じです。

この性質により、「最初にサスペンドしたレンダリング」から「サスペンド明けのレンダリング」に情報を渡すことは自力では困難です。そのため、最初のレンダリングの中で作ったFetcherインスタンスをサスペンド明けのレンダリングで手に入れることができず、サスペンドが空けても何をレンダリングすればいいか分からなくなってしまいます。Fetcherをpropsで外から受け取ることでこの問題は回避できるのです。

サスペンドとコンポーネントの純粋性

useStateがだめならuseRefなら、と思った方もいるかもしれませんが、実はuseRefでも無理です。useRefはレンダリングをまたいで同じオブジェクトを返すのが特徴でしたが、useRefによって返されるオブジェクトは最初のレンダリングで作られます。よって、「最初のレンダリング」が何回も繰り返されれば毎回新しいrefオブジェクトが作られることになり、やはりサスペンド前後の情報の受け渡しは困難です。

ただし、最初のレンダリング以外の場合は注意が必要です。そもそも最初のレンダリング以外であっても、サスペンドしたレンダリングの結果は残りません。例えば、useMemoはサスペンドしたレンダリングにおいて計算された値はキャッシュしません。そのレンダリング中に値を計算したという事実が無かったことにされるからです。

しかし、useRefは「毎回同じオブジェクトを返す」のが役割ですから、初回以外であればサスペンドしたレンダリングとサスペンド明けのレンダリングではuseRefから同じオブジェクトが返されます。これを用いることで、サスペンドしたレンダリングから何らかの情報を残すことができます。

明らかに、このようなことは避けるべきです。それは、このようなuseRefの使用はレンダリングの純粋性を破壊しているからです。レンダリングの純粋性とは、「コンポーネントをレンダリングしても副作用が発生しない」という意味で、「意味もなくコンポーネントをレンダリングしても(=関数コンポーネントを関数として呼び出しても)安全である」という意味でもあります。

Concurrent Modeにおいては「コンポーネントがレンダリングされた(関数コンポーネントとして呼び出された)」ことは「そのコンポーネントのレンダリング結果がDOMに反映される」ことを意味しません。サスペンドが発生する可能性があるからです。この状況下でReactが好き勝手にレンダリングを試みるための前提として、コンポーネントは純粋であるべきとされているのです。

実際、Reactでは副作用はuseEffect内で行うように推奨しています。useEffectはコンポーネントが実際にDOMにマウントされた場合にコールバックが呼び出されます。サスペンドによりDOMに反映されなかった場合はコールバックは発生しません。

また、レンダリングが純粋であることを強調するためか、Conncurrent Modeではデフォルトで1回のレンダリングで関数コンポーネントが2回呼び出されるようになっています(おそらくproductionでは1回)。これは、純粋でないコンポーネントを作ってしまった際に発生するバグを検出しやすくするためでしょう。実は先ほどのuseStateのサンプルでも、1回PageBがレンダリングされるたびにFetcherインスタンスが2個作られていました。非同期処理を発生させるのも副作用ですから、そもそもuseStateのステート初期化時にFetcherインスタンスを作るのは無理筋だったということになります。

useRefに話を戻しますが、Concurrent Modeではrefオブジェクトへのアクセス(特に書き込み)は副作用であると考えるべきです。先ほど説明したように、レンダリング中にrefオブジェクトに書き込むと、サスペンドしたレンダリングの影響がそれ以降に残ってしまうため、コンポーネントのレンダリングが純粋でなくなるからです。refオブジェクトは、useEffectのコールバック内やイベントハンドラなど、副作用が許された世界でのみアクセスすべきです。refオブジェクトはもはや完全に副作用の世界の住人なのです。

目ざとい方は、先程出てきたuseObjectIduseRefに書き込んでいたじゃないかと思われるかもしれません。それはそのとおりなのですが、実はuseObjectIdはレンダリングの純粋性を損なわないように注意深く実装されています。純粋性を壊さない注意深い実装ならば、useRefを使える可能性もあるのです。無理なときは無理なので無理だと思ったら潔く諦めるべきですが。

まとめ

この記事では、サスペンドを念頭に置いたコンポーネント設計をどのようにすべきかについて議論しました。

重要なのは、サスペンドはその発生の仕方によって3種類に分類できるということです。さらに、これらを組み合わせることでより複雑なパターンを実装することもできます。もちろん、コンポーネントの記述は宣言的な書き方が保たれています。

Concurrent Modeでは、あるコンポーネントがどのような状況下でどの種類のサスペンドを発生させるのかということをコンポーネント仕様の一部として考えなければなりません。これは特にuseTransitionと組み合わせるときに重要です。Concurrent Mode時代のコンポーネント設計では、コンポーネントの責務は何なのかということを冷静に見極めて、そのコンポーネントはどのようにサスペンドすべきかということを考えなければならないのです。

記事の後半では、Concurrent Modeでは特にレンダリングの純粋性が重要であることを開設しました。これを踏まえると、初手でサスペンドするコンポーネントは必然的にFetcherを外部から受け取ることになります。

次回の記事では、誰がFetcherを作ってどう受け渡すのかについて考えていきます。

次の記事: 鋭意執筆中です。


  1. このコンポーネントの外部に設置された既存のSuspenseにキャッチ場合はuseTransitionに反応しないサスペンドとなりますが、それはこのコンポーネントの預かり知るところではありません。このコンポーネントがuseTransitionに対応する可能j性を消しているわけではないことからYesとしています。 

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

FullCalendarとIndexedDB

javascriptのカレンダーライブラリFUllcalendarを用いてアプリを作成しています。
(公式URL : https://fullcalendar.io)

IndexedDBに保存しているデータをカレンダーにイベントとしてしようとして以下を実行致しました。
以下の例ではdateが'2020/4/5'から'2020/4/10'のデータを抽出しています。

  //オブジェクトの定義
  var calendar;
  //IndexedDBの定義
  var db = new Dexie("testDB");
  db.version(1).stores({
      testTable: '++id,worker,date,notes'
  });
    db.testTable
    .where('date')
    .between('2020/4/5','2020/4/10',true,true)
    .uniqueKeys()
    .then(
      (r) => {
        for (var i =0; i<r.length; i++)
        {
          calendar.addEvent({
            title: 'test',
            start: r[i],
            allDay: true
          });
        }
        calendar.render();
    });

しかし、データは取得できていますが、カレンダーにはイベントが表示されません。
どのようにすればIndexedDBから取得したデータをカレンダーに表示出来るでしょうか?
お分かりになられる方がいらっしゃいましたら何卒ご助言宜しくお願い致します。

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

javascriptの代入の基礎(深いコピー、浅いコピー)

1. string, number, booleanの代入

配列やオブジェクト以外を代入する場合、値が代入される。

let ori_num = 10;
let new_num = ori_num; // 変数の場合数字を代入
ori_num = 20;

console.log(ori_num, new_num); //output => 20, 10

2. 配列arrayの代入

配列を代入する場合、代入される配列は元の配列を参照する。
そのため、元の配列(ori_arr)の値が変更されると、代入される配列(new_arr)の値も変更されてしまう。

const ori_arr = ['aaa', 'bbb'];
const new_arr = ori_arr; // 元の配列を参照
ori_arr[1] = 'ccc'; // 元の配列の値が変わると…

// 新しく作成した配列も変わる!
console.log(ori_arr,new_arr); // output => ["aaa","ccc"], ["aaa","ccc"]

この対策として以下の4つの方法がある。

対策1:sliceでコピー

const ori_arr =['aaa', 'bbb'];
const new_arr = ori_arr.slice();
ori_arr[1] = 'ccc'; //元の配列の値を変えても...

// 新しく作成した配列には反映されない!
console.log(ori_arr, new_arr); // output => ["aaa","ccc"] ["aaa","bbb"]

対策2: 新しい配列作成+concat

const ori_arr =['aaa', 'bbb'];
const new_arr = [].concat(ori_arr);
ori_arr[1] = 'ccc'; //元の配列の値を変えても...

// 新しく作成した配列には反映されない!
console.log(ori_arr, new_arr); //["aaa","ccc"] ["aaa","bbb"]

対策3: ES6 spread

const ori_arr =['aaa', 'bbb'];
const new_arr = [...a];
ori_arr[1] = 'ccc'; //元の配列の値を変えても...

// 新しく作成した配列には反映されない!
console.log(ori_arr, new_arr); //["aaa","ccc"] ["aaa","bbb"]

対策4: Array#from

const ori_arr =['aaa', 'bbb'];
const new_arr = Array.from(ori_arr);
ori_arr[1] = 'ccc'; //元の配列の値を変えても...

// 新しく作成した配列には反映されない!
console.log(ori_arr, new_arr); // output => ["aaa","ccc"] ["aaa","bbb"]

3. Objectの代入

オブジェクトを代入する場合、代入されるオブジェクトは元のオブジェクトを参照する。
そのため、元のオブジェクト(ori_obj)の値が変更されると、代入されるオブジェクト(new_obj)の値も変更されてしまう。

const ori_obj = {
    a:"aaa"
}
const new_obj = ori_obj; //元のオブジェクトに代入
ori_obj.a = "bbb"; //元のオブジェクトの値を変えると...

// 新しく作成したオブジェクトも変更される!
console.log(ori_obj, new_obj); // output => {a:"bbb"}, {a:"bbb"}

そのための対策方法は下記の通り。

対策?assignを使用(浅いコピー)

Object#assignは浅いコピー(shallow copy)のため、
オブジェクトの中のオブジェクトの値(下記の例だとshallow.b.c)は元のオブジェクトobjに依存してしまう。

const obj = {
    a : 'aaa',
    b: {
        c: 'ccc'
    }
}
//浅いコピー (shallow copy)
const shallow = Object.assign({}, obj);
obj.a = 'change';  // オブジェクトを変更
obj.b.c ='change'; // オブジェクトの中のオブジェクトを変更

console.log(obj, shallow);
// output
// obj     = {a:"change",b:{c:"change"}}
// shallow = {a:"aaa",   b:{c:"change"}} オブジェクトの中のオブジェクトは変更される

そこで、以下のような実装でこの問題を解決する。

対策:JSON.parse(JSON.stringify(obj))を使用(深いコピー)

JSON#parsestringJSONに変換し、
JSON#stringifyJSONstringに変換する。

const obj = {
    a : 'aaa',
    b: {
        c: 'ccc'
    }
}
//深いコピー (deep copy)
const deep = JSON.parse(JSON.stringify(obj));
obj.a   ='change';
obj.b.c ='change';

console.log(obj, deep);
// output
// obj     = {a:"change",b:{c:"change"}}
// shallow = {a:"aaa",   b:{c:"ccc"   }} 

JSON.parse(JSON.stringify(obj))が深いコピーになる理由

// JSON.stringify(obj)が
JSON.parse("{aaa:bbb,{ccc:ddd}}");
// のようにいったん文字列に置き換えているから
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

時間を持て余す小3がママのプログラムを手伝った話

きっかけ

Webデザインをしている嫁が、ビデオ通話でマイクボリュームのデザインをしていて、サンプルを動作こみで社内レビューに出そうとしてました。
こんなデザインです。
■■■■□□□
1秒毎にこうなったり、
■□□□□□□
こうなったり。
■■■■■□□

プログラマーであればJavascriptですぐに実装できますが、Webデザイナーである嫁は何から手をつけていいかわからず。当然プログラマー出身SEの僕にどうしたらいいか聞いてきます。

えーっと正直めんどくさいw
そもそもいきなり構文を書くよりロジックというものを分かってもらわないと今後の為になりません!
ということで、コロナの影響で休校中の暇な小3の息子にサンプルを作らせましたw

利用させたツール

DeNAさんが提供している「プログラミングゼミ」です。
00.jpg

実際にコードを書かずに、ブロックの組み立て(フローチャート)でアプリケーションを作るというものです。
視覚的にロジックが分かるので息子の作り上げたロジックをまずは嫁に理解してもらおうという作戦です。

僕の要求

  • 1が押されたらランプが1個光る
  • 2が押されたらランプが2個光る
  • 3が押されたらランプが3個光る
  • 4が押されたらランプが4個光る
  • 5が押されたらランプが5個光る

これを指示しました。
結果、基本デザインは以下のようになりました。
Screenshot_20200403-234740.png

ランプ=電球を僕はイメージしてましたが、息子の中では魔法のランプと受け取ったようです。

実装部分

まずは呼び出し部分です。ボタンにこのロジックが付与されています。
Screenshot_20200404-000232.png
ボタンをタッチされたら何かしらのデータを送っています。

次に受け取り部分です。
Screenshot_20200403-234730.png
それぞれのランプは値を受け取ったあとにお化け?を呼び出すようになっています。(=光る)

まあ、実際に引数の数字にあまり意味はありませんが、そこは見なかった事に。

この呼び出しと受け取りが5セット存在しているプログラムになりました。

完成品

GIFアニメーションにしてみました。実際にクリックしている様子は見えませんが、3、4、2をクリックしています。
ezgif-4-cf3fc2e32d84.gif

作成依頼をして30分くらいでつくりあげました。
とうぜん他にもいろいろなロジックがあるんですが、これはこれですごくよくできていると思います。

結局のところ。。。

嫁が実装したプログラムはfor文使ってました。
(for文の中に余分なコードももあってやっぱりよく理解できてなさそうでした)
途中まではできてましたが、結局うまく動かない部分があり、仕上げは僕が。。。

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