LoginSignup
0
0

More than 1 year has passed since last update.

MicrosoftのWeb開発教材を使ってみた ⑤-2ブラウザ拡張機能 【Promise/API/LocalStorage/拡張機能作成/BackGround/Performance】

Last updated at Posted at 2022-02-12

はじめに

「Web Development For Beginners」というMicrosoftがGithubに公開している教材についての記事です。

教材の紹介・選んだ理由など

この教材を選んだ理由

https://github.com/microsoft/Web-Dev-For-Beginners

  • HTML/CSS/JavaScriptを触れるいい感じの教材が欲しかった
    • そこそこのボリュームがあり、作りながら学べるタイプの教材
    • 基礎的なトピックが一通り網羅されている
  • 質が高そう
    • なにせあのMicrosoftなので、きっと良いものでしょう。
  • 題材が面白そう
    • 軽く調べた感じだとチュートリアルでよくある題材として「TODOアプリ」「クイズアプリ」などがあるみたいですが、どれもどう実装するのか想像がついてしまって、余り興味がわきませんでした。
    • しかしこの教材は「テラリウム」「タイピングゲーム」「ブラウザ拡張機能」「スペースゲーム」「銀行プロジェクト」と、面白そうなトピックが並んでいます。

+α 実際に取り組んで感じたこと

  • 提供されるリファレンス・参考サイトの質が高い
    • 一例はFlexbox Froggy。🐸 を並べながら flexbox の扱いについて学べるサイトです。超わかりやすいです。

https://flexboxfroggy.com/#ja

  • 「アクセシビリティ」「ブラウザがどう動くのか」といった知識も学べる
    • 絶対やるべきだけど後回しにしがちなトピックも結構ガッツリ触れます。
    • かゆいところに手が届く感じ。
  • 多分、英語全くわからなくてもなんとかなる
    • ほとんどのレッスンは translationsというフォルダに日本語訳があります。
    • 最悪全部Deeplに突っ込めばなんとかなります。
  • Edge推しがすごい
    • Microsoftの教材なので当然ですが、デモでは基本Edgeが使われます。
  • スケッチノートがわかりやすい
    • 一部レッスンは最初にスケッチノートというイラストがあるのですが、それがすごくわかりやすいです。それに可愛い。
    • 扱うトピックについてイラストで視覚的に示してくれるので、どんな内容をやるのかざっくり把握してからレッスンに入ることが出来ます。

image.png

microsoft/Web-Dev-For-Beginners/tree/main/1-getting-started-lessons/3-accessibility より

教材の概要

各レッスンに以下の要素が含まれます。

  • スケッチノート(オプション)
    • レッスンの概要がわかりやすくまとまったイラスト
  • 補足のビデオ(オプション)
  • レッスン前の小テスト
    • 簡単なテスト
  • ステップバイステップなレッスン
  • 知識のチェック
  • レッスン後の小テスト
    • 簡単なテスト
  • チャレンジ
  • 副読本(サイト)
  • 復習と自己学習
  • 課題

チャレンジ〜は調べ物や課題をこなします。
課題については必要だと思ったものだけやりました。

教材の構成

  1. getting-started-lessons(はじめに)
    1. プログラミング言語と開発ツール
    2. アクセシビリティ
    3. Githubの基礎
  2. js-basics(JavaScript基礎)
    1. データ型
    2. 関数とメソッド
    3. 分岐処理
    4. ループ
  3. terrarium(テラリウム構築)
    1. HTMLイントロ
    2. CSSイントロ
    3. DOM操作とクロージャ
  4. typing-game(タイピングゲーム)
    1. タイピングゲームを作る(イベント管理)
  5. browser-extension(ブラウザ拡張機能)
    1. ブラウザについて
    2. API呼び出し、ローカルストレージの利用
    3. バックグラウンドタスクとパフォーマンス
  6. space-game(スペースシューティングゲーム)
    1. イントロ(Pub-Subパターン)
    2. キャンバス
    3. モーションの追加
    4. レーザー追加、衝突検出
    5. スコアの保存
    6. 終了と再起動
  7. bank-project(架空の銀行プロジェクト)
    1. WebアプリのHTMLテンプレートとルート
    2. ログインと登録フォームの構築
    3. データの取得と利用方法
    4. 状態管理の概念

取り組む際に気をつけたこと

  • コピペ/写経にならないようにする
    • サンプルコードと実装の解説が一緒になっているので、理解したつもりになってコピペしがちです。
    • まず一通り目を通してから、なるべく自分の頭で考えて実装するようにしました。
  • 全部完璧にやろうとしない
    • 「12週間、24レッスンのカリキュラム」と銘打たれているように、出される課題や副教材を全てこなそうと思うとかなりボリュームがあります。
      • そのため、現時点で必要だと思うカリキュラムにのみ取り組みました。


〜②JavaScript基礎まで【導入/アクセシビリティ/JavaScript の基礎】
③テラリウム構築 【HTML・CSS基礎/DOM操作/クロージャ】
④タイピングゲーム 【JavaScriptのイベント処理】
⑤-1ブラウザ拡張機能 【ブラウザの仕組み/拡張機能作成の導入】
⑤-2ブラウザ拡張機能 【API/LocalStorage/BackGround/Performance】 本記事
⑥スペースシューティングゲーム 【ゲーム開発の基礎/Pub-Sub/Canvas/衝突検出】
⑦-1銀行プロジェクト【SPA//HTMLフォーム】
⑦-2銀行プロジェクト【ログイン/データ管理/状態管理】


記事の目的

  • 学習のアウトプット
  • 教材を使ってみたところかなり良かったので、その紹介

注意点

自身の学習のアウトプットがメインなので、理解できているところ(他言語と共通の箇所など)は省いています。
また、課題やtipsについても結構省きます。
この教材に興味を持った方はぜひご自分で取り組んでみてください。

5-Browser-extensionの続き

指定された地域のカーボンフットプリントを表示するブラウザ拡張機能を作っていきます。
その過程でブラウザの仕組みやフォームの構築方法、APIの呼び出し、ローカルストレージの使用方法などについて学びます。

browser-extension-demo

https://github.com/microsoft/Web-Dev-For-Beginners/tree/main/5-browser-extension より

学習の目的

  • ブラウザの仕組み、歴史、ブラウザ拡張機能の最初の要素の足場の作り方を学ぶ
  • ローカルストレージに格納された変数を使用してAPIを呼び出すためのブラウザ拡張機能のJavaScript 要素を構築する
  • ブラウザのバックグラウンドプロセスを使用して拡張機能のアイコンを管理する
  • Webのパフォーマンスと最適化について学ぶ

フォームの構築、API の呼び出し、ローカルストレージへの変数の格納

  • フォームを構築する。
  • APIを呼び出し、結果を表示してみる。
  • ローカルストレージの使い方を学ぶ。

src/index.js に実装していく。

HTMLエレメントへの参照

// querySelector : 指定されたセレクタに一致する最初の要素を返す
const form = document.querySelector('.form-data');
const region = document.querySelector('.region-name');
// 以下省略

イベントリスナーの追加

form.addEventListener('submit', (e) => handleSubmit(e));
clearBtn.addEventListener('click', (e) => reset(e));
init();

init reset 関数の実装(localStorageの利用)

初期設定、リセット用の関数を定義する。
特定の要素の表示/非表示はdisplayを編集することで実装する。

  • ユーザーがAPIKeyリージョンコードをローカルストレージに保存しているかどうかチェック
  • APIキーかリージョンコードのいずれかが NULL の場合、
    • 入力フォームを表示
    • results、loading、clearBtnを非表示
    • エラーテキストを空文字列に設定
  • APIキーとリージョンコードが存在する場合、以下の処理を行う
    • APIを呼び出して炭素使用量データを取得する
    • 結果エリアを隠す
    • フォームを隠す
    • リセットボタンを表示する

今回の例では、データの保存にLocalStorageを使用する。

  • ブラウザに[キー:値]のペアとしてデータを保存しておくことができる機能
  • SessionStorageと異なり期限切れがない
  • ブラウザ拡張は独自のローカルストレージを持つ。メインブラウザウィンドウは別のインスタンスであり、別々に動作する。
  • getItem() setItem() removeItem()のいずれかを使用して操作する
  • 学習用なので平文のままのAPIキーをローカルストレージに保存するが、本来それは避けるべき
function init() {
  //ローカルストレージに保存されているキーを取得
  const storedApiKey = localStorage.getItem('apiKey');
  const storedRegion = localStorage.getItem('regionName');

  if (storedApiKey === null || storedRegion === null) {
    //いずれかのキーが空ならフォームを表示
    form.style.display = 'block';
    results.style.display = 'none';
    loading.style.display = 'none';
    clearBtn.style.display = 'none';
    errors.textContent = '';
  } else {
    //キーがローカルストレージに保存されていれば結果を表示
    displayCarbonUsage(storedApiKey, storedRegion);
    results.style.display = 'none';
    form.style.display = 'none';
    clearBtn.style.display = 'block';
  }
};

function reset(e) {
  e.preventDefault();
  //「地域」をローカルストレージから削除
  localStorage.removeItem('regionName');
  init();
}

フォーム送信の処理を追加

  • preventDefault でイベントの伝搬を阻止
    • イベント = submit イベント。
    • デフォルトだとフォームの内容をURLに送信=ページが更新されてしまうため、それを阻止している。
    • 参考 - MDN
  • 新しく追加する setUpUser 関数にapiKey, regionを渡す
function handleSubmit(e) {
  e.preventDefault();
  setUpUser(apiKey.value, region.value);
}

// 呼び出される場所
form.addEventListener('submit', (e) => handleSubmit(e));

setUpUser関数でローカルストレージにAPIキーとリージョンの値を設定

  • API呼び出し中に表示されるロード画面を設定
function setUpUser(apiKey, regionName) {
  localStorage.setItem('apiKey', apiKey);
  localStorage.setItem('regionName', regionName);
  loading.style.display = 'block';
  errors.textContent = '';
  clearBtn.style.display = 'block';

  displayCarbonUsage(apiKey, regionName);
}

APIへの問い合わせ、炭素使用量の表示

  • API Application Programming Interface
    • ソフトウェアの一部機能を共有する仕組みのこと
    • 機能を公開している側⇔利用したい側をつなぐインターフェース
    • RESTAPIが最も一般的で、HTTPリクエスト/URLなどを利用してデータ(一般的にはJSONファイル)をやり取りする
  • async/await 非同期処理
    • 「データを返す」などの処理が完了するのを待ってから処理を続ける
    • APIの応答速度を制御できないため
  • レスポンスの内容を表示する
    • エラー、内容が無い場合はエラーメッセージを表示する
import axios from '../node_modules/axios';

async function displayCarbonUsage(apiKey, region) {
try {
    await axios
        .get('https://api.co2signal.com/v1/latest', {
            params: {
                countryCode: region,
            },
            headers: {
                'auth-token': apiKey,
            },
        })
        .then((response) => {
            let CO2 = Math.floor(response.data.data.carbonIntensity);

            loading.style.display = 'none';
            form.style.display = 'none';
            myregion.textContent = region;
            usage.textContent =
                Math.round(response.data.data.carbonIntensity) + ' grams (grams C02 emitted per kilowatt hour)';
            fossilfuel.textContent =
                response.data.data.fossilFuelPercentage.toFixed(2) +
                '% (percentage of fossil fuels used to generate electricity)';
            results.style.display = 'block';
        });
} catch (error) {
    console.log(error);
    loading.style.display = 'none';
    results.style.display = 'none';
    errors.textContent = 'Sorry, we have no data for the region you have requested.';
}
}

この関数はasync キーワードが使われている。これは、関数が非同期で実行されることを表す。

  • データが返されるなどのアクションが完了するのを待ってから処理を続けることを意味する。
  • try/catchブロックが含まれており、APIがデータを返したときにPromiseを返すようになっている。APIが応答する速度を制御できないので(全く応答しないかもしれない)、非同期で呼び出すことによってこの不確実性を処理している。

また、axiosも使われている。公式によると、

Promise based HTTP client for the browser and node.js

直訳するとブラウザや node.js で動く Promise ベースの HTTP クライアント。

  • 簡潔にHTTP通信の処理が書けることや、ブラウザ・node.jsに対応していることが利点らしい。
  • ここではgetメソッドを使ってresponseを得ている。

    // `data` is the response that was provided by the server
    data: {},
    
  • paramsheadersなどに値を渡すことで色々と設定できる。

  // `headers` are custom headers to be sent
  headers: {'X-Requested-With': 'XMLHttpRequest'},

  // `params` are the URL parameters to be sent with the request
  // Must be a plain object or a URLSearchParams object
  params: {
    ID: 12345
  },
  • 試しにQiitaにアクセスする時のHTTPヘッダを見てみる headers

Promise is 何

わからないこと

  • Promise
  • 非同期
  • async/await

Promise

プロミス (Promise) は、非同期処理の最終的な完了もしくは失敗を表すオブジェクトです。
プロミスの使用 - MDN

ふむ。非同期とは・・・

非同期

非同期とは?

通常は、あるプログラムのコードは書かれた順に、一度にひとつのことだけが起こるように実行されます。もしある関数が別の関数の結果に依存するのであれば、その関数は他の関数の処理が完了して結果を返すまで待たなくてはならず、それまでは、ユーザー視点からはプログラム全体は止まっているのと本質的には同じです。

例えば、Mac ユーザーは回転する虹色のカーソル(よく「ビーチボール」と呼ばれます)としてこのことを経験することもあるでしょう。このカーソルによってオペレーティングシステムは「現在使用中のプログラムは何かが終わるのを待って停止しており、それが非常に長く掛かっているので何が起こっているのかとご心配をお掛けしているのではないでしょうか」と言っているのです。
RAINBOW

これはいら立つような体験であり、コンピューターの処理能力の良い使い方ではありません――特に、マルチコアプロセッサーが利用できる時代においては。他のタスクを別のプロセッサーコアに処理させて、それが終わった時に知らせることができるのに、座って待っているのは意味がありません。このように合間に別の仕事を終わらせる、ということが非同期プログラミングの基本です。非同期にタスクを実行する API は、あなたが使用するプログラミング環境(ウェブ開発であればウェブブラウザー)によって提供されます。

  • ある処理をしている間に、他の処理を進めること(=処理が同期していない)を非同期処理と呼ぶ。
  • 一方、順次実行していくタイプの処理は同期処理と呼ぶ。
  • 同期処理の場合、高負荷な処理に差し掛かった際に全体の処理が止まってしまうという弱点がある。ブラウザでは特にその弱点が問題になるため、非同期処理を使う場面が多い。

スレッド

プログラムがタスクを完了させるのに使用できる単一のプロセスのこと。
各スレッドは1度に1つのタスクを実行することしかできない。

Task A --> Task B --> Task C

この場合、各タスクは順次実行される。

現在多くのコンピューターは複数のコアを持つため、同時に複数のタスクを処理できる。

Thread 1: Task A --> Task B
Thread 2: Task C --> Task D

しかし、JavaScriptはシングルスレッドなのでメインスレッドと呼ばれる単一のスレッド上でタスクを実行できるだけ。

Main thread: Task A --> Task B

Web workers によって worker と呼ばれる別個のスレッドに JavaScript の処理の一部を移すことが可能となり、複数のコードを同時に実行できるようになった。(マルチスレッドのこと、非同期処理とは別)

Main thread: Task A --> Task C
Worker thread: Expensive task B

workerにはいくつか問題があった(DOM操作ができないなど)ので、その解決のためにブラウザーを利用して特定の処理を非同期に実行する。
具体的には、Promiseのような機能を利用することで、ある処理(例:サーバーからの画像の取得)を実行し、その結果が返ってくるまで別の処理の実行を待たせる、といったことが可能。

Main thread: Task A               Task B
Promise:      |__async operation__|

この Promise の処理はどこか別の場所で行われるため、非同期処理が実行されている間にメインスレッドがブロックされることは無い。

改めてPromise

プロミス (Promise) は、非同期処理の最終的な完了もしくは失敗を表すオブジェクトです。

  • プロミスを使った非同期処理では、本来返したい戻り値の代わりにPromiseというオブジェクトを返しておき、本来返したい値を渡せる状態になったら、そのプロミスオブジェクトを通して呼び出し元に値を渡す。
  • 非同期処理に、処理の順番の約束(=プロミス)を取り付けるようなイメージ。

引数

引数には関数を渡し、その関数の引数としてresolvereject(関数)を渡す。

new Promise(function (resolve, reject){
  // 非同期処理
})

それぞれ以下のような役割。

  • resolve:非同期処理が解決したことを知らせる。
    • returnの代わりにresolve()と記述することで、非同期関数が解決したことを知らせる。
  • reject:非同期処理が拒否されたことを知らせる。
    • returnの代わりにreject()と記述することで、非同期関数が拒否されたことを知らせる。
    • 任意。

3つの状態

Promiseには3つの状態がある。

  • pending:非同期処理の実行中の状態を表す
  • fulfilled:非同期処理が解決した状態を表す
  • rejected:非同期処理が拒否された状態を表す
new Promise(function (resolve, reject){
  try {
    resolve(); // ==> fulfilled
  } catch (e) {
    reject(); // ==> rejected
  }
})

動作

動作させてみる。
とりあえずPromiseを利用しないパターン。
setTimeoutの処理の完了は待たれない。

console.log("1番目");

// 1秒後に実行する処理
setTimeout(() => {
  console.log("2番目(1秒後に実行)");
}, 1000);

console.log("3番目");

// > 1番目
// > 3番目
// > 2番目(1秒後に実行)

次に、Promiseを使ったパターン。
こちらは処理の完了を待つ。

console.log("1番目");

new Promise((resolve) => {
  //1秒後に実行する処理
  setTimeout(() => {
    console.log("2番目(1秒後に実行)");
    resolve();
  }, 1000);
}).then(() => {
  console.log("3番目");
});

// > 1番目
// > 2番目(1秒後に実行)
// > 3番目

then/catch

返り値に対してはthen(解決された時呼ばれる)catch(拒否された時呼ばれる)が使える。

  • pendingresolve なら then が呼ばれる。
  • pendingreject なら catch が呼ばれる。
  • 返り値はPromise
    • catchの場合もfulfilledPromiseが返ってくる。
function asyncFunction() {
  return new Promise((resolve, reject) => {
    try {
      setTimeout(() => {
        console.log("1秒経過")
        resolve();
      }, 1000)
    } catch (e) {
      // 拒否されたときの処理
      reject();
    }
  })
}

asyncFunction().then(() => {
  console.log("resolve後の処理");
}).catch(e => {
  console.log("reject後の処理");
})

// > Promise {<pending>}
// > 1秒経過
// > resolve後の処理

非同期処理中に取得した値を使いたい場合

  • then/catchは引数を取ることができる。
  • resolve/rejectに引数を渡すことで、次に呼ばれるthen/catchの第1引数に渡すことが出来る。
function asyncFunction() {
  return new Promise((resolve, reject) => {
    try {
      setTimeout(() => {
        console.log("1秒経過")
        const num = 1 // 何らかの値を取得したとする
        resolve(num); // 取得した値をthenに渡す
      }, 1000)
    } catch (e) {
      reject(e);      // キャッチしたエラーを渡す
    }
  })
}

asyncFunction().then( num => {
  console.log(`引数で受け取った値:${num}`);
}).catch(e => {
  console.log(`引数で受け取った値:${e}`);
})

// > Promise {<pending>}
// > 1秒経過
// > 引数で受け取った値:1

プロミスの連鎖

  • 非同期処理の連鎖(前の処理が成功したとき、その結果を使って後続の操作を開始)が可能。
    • thencatchは新しいPromiseを返すため。
    • 非同期処理.then().catch()といった形でメソッドチェーンを使う事もできる。
  • thenは最大2つの引数を取る。
    • 1番目の引数 プロミスが解決した場合のコールバック関数
    • 2番目の引数 拒否された場合のコールバック関数
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('foo');
  }, 300);
});

myPromise
  .then(handleResolvedA, handleRejectedA)
  .then(handleResolvedB, handleRejectedB)
  .then(handleResolvedC, handleRejectedC)
  .catch(handleRejectedAny);

アロー関数で表すとこうなる。

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('foo');
  }, 300);
});
promise1
.then(value => { return value + ' and bar'; })  //resolveに渡した'foo'が引数
.then(value => { return value + ' and bar again'; })
.then(value => { console.log(value) })
.catch(err => { console.log(err) });

// > Promise {<pending>}
// > foo and bar and bar again

Promise.all

配列としてPromiseオブジェクトを渡す。
allに渡された全てのPromiseresolveされたら次の処理に進む。

Promise.all([promise1, promise2]).then(() => {
  console.log("全部終了!");
});

Promise.race

allと同じく配列としてPromiseを渡す。
どれか一つでもresolveされたら次に進む。


Promise.race([promise1, promise2]).then(() => {
  console.log("どれか一つ終了!");
});

async/await

async / awaitPromiseの糖衣構文。

糖衣構文(syntax sugar)

シンタックスシュガーとは、プログラミング言語で、ある構文を別の記法で記述できるようにしたもの。長い構文を簡略に記述できるようにしたり、複雑な構文を読み書きしやくするために用意される。
IT用語辞典

function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
}

asyncCall();
// => "calling"
// => "resolved"
  • asyncから関数式を書き始めることで、非同期関数を定義できる。
    • 値をreturnした場合、Promiseは返り値をresolveする。
    • 例外、何かしらの値をthrowした場合はその値をrejectする。
  • 関数の前にawaitを置くことで、その関数のプロミスが解決するか拒否されるまで処理を一時停止する。
    • awaitは必ずasync function内で使う。
    • この動作を利用して、プロミスを返す関数をあたかも同期しているかのように動作させることが出来る。

具体例

参考 : 連続した非同期処理

  • 画像の読み込みなど、前の処理の完了を待つ必要がないような場合には連続した非同期処理を使わない方が良い。
function sampleResolve(value) {
  return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 2000);
    })
}

async function sample() {
    return await sampleResolve(5) * await sampleResolve(10) + await sampleResolve(20);

async function sample2() {
    const a = await sampleResolve(5);
    const b = await sampleResolve(10);
    const c = await sampleResolve(20);
    return a * b + c;
}

sample().then((v) => {
    console.log(v); // => 70
});

sample2().then((v) => {
    console.log(v); // => 70
});

参考 : 並列の非同期処理

並列の非同期処理(Promise構文)

function sampleResolve(value) {
  return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 2000);
    })
}

function sampleResolve2(value) {
  return new Promise(resolve => {
        setTimeout(() => {
            resolve(value * 2);
        }, 1000);
    })
}

function sample() {
    const promiseA = sampleResolve(5);
    const promiseB = sampleResolve(10);
    const promiseC = promiseB.then(value => {
        return sampleResolve2(value);
    });

    return Promise.all([promiseA, promiseB, promiseC])
        .then(([a, b, c]) => {
            return [a, b, c];
        });
}

sample().then(([a, b, c]) => {
    console.log(a, b, c); // => 5 10 20
});

並列の非同期処理(async/await構文)

Promise.allにもawaitを利用できるため、以下のように記述できる。

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 2000);
    })
}

function sampleResolve2(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value * 2);
        }, 1000);
    })
}

async function sample() {
    const [a, b] = await Promise.all([sampleResolve(5), sampleResolve(10)]);
    const c = await sampleResolve2(b);

    return [a, b, c];
}

sample().then(([a, b, c]) => {
    console.log(a, b, c); // => 5 10 20
});

array.map()も利用できる。

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 2000);
    })
}

async function sample() {
    const array =[5, 10, 15];
    const promiseAll = await Promise.all(array.map(async (value) => {
        return await sampleResolve(value) * 2;
  }));

    return promiseAll;
}

sample().then(([a, b, c]) => {
    console.log(a, b, c); // => 10 20 30
});

課題 APIを使ってなにか作ってみる

無料のAPIを使用して拡張機能を構築してみる

Emoji-haveを使って拡張機能を作る

Emoji-haveというAPIを使ってみます。

以下のようなことが出来るAPIです。面白そうですね。

  • カテゴリ別に絵文字を取得
  • ランダムに取得/全て取得

なにか絵文字を使いたいけど具体例が思いつかない・・・
というときに使える拡張機能を作りました!そんな時無いけど

イメージ

カテゴリ別にボタンを用意しておく
→ボタンを押す
→対応するカテゴリの絵文字を表示する
→コピーできる

実装

ボタンを追加

<div class="btn-field">
    <button class="search-btn">smileys_and_people</button>
    <button class="search-btn">animals_and_nature</button>
    <button class="search-btn">food_and_drink</button>
    <button class="search-btn">travel_and_places</button>
    <button class="search-btn">activities</button>
    <button class="search-btn">objects</button>
    <button class="search-btn">symbols</button>
    <button class="search-btn">flags</button>
</div>

JSからボタン取得

  • 複数なので getElementByClassName を使ってみる
const buttons = document.getElementsByClassName("search-btn")

console.log(buttons[0].textContent) // => smileys_and_people

ボタンにイベント追加

  • forEach で追加を試みる
buttons.forEach(button => {
    button.addEventListener('click', clickButton)
});

error

・・・あれ?

MDN - indexed collection

どうやらHTMLCollectionには forEach メソッドがないようです。
代わりに以下のように呼び出します。

Array.prototype.forEach.call(buttons, button => {
    button.addEventListener('click', clickButton)
});

イベントからボタンのテキストを取得

  • event.target.textContent でボタンのテキストを取得します。
  • ボタンのテキストを絵文字をゲットする関数に渡して何かします。
const clickButton = e => {
    // イベントからボタンのテキストを取得
    const buttonText = e.target.textContent
    // 何かする
    getRandomEmoji(buttonText)
}

絵文字APIを叩く関数

  • ひとまず実行してみます。
const getRandomEmoji = async (buttonText) => {
    try {
        await axios
            .get(`https://emojihub.herokuapp.com/api/random/category_${buttonText}`)
            .then((response) => {
                console.log(response)
                loading.style.display = 'none';
                results.style.display = 'block';
            });

レスポンスは以下。
api-get-result
どれが絵文字・・・?

READMEを見に行きます。

Emoji-have-README

どこかにname, htmlCodeなどのデータがまとめて入っている感じのようです。
中身を見てみた感じだと、 data > htmlCode/unicode に入ってるみたいですね。

image.png

試しにHTMLに貼ってみます。

trying-html-paste

OK!

実際に表示してみます。

const clickButton = e => {
    // イベントからボタンのテキストを取得
    const buttonText = e.target.textContent
    getRandomEmoji(buttonText)
}

const getRandomEmoji = async (buttonText) => {
    try {
        await axios
            .get(`https://emojihub.herokuapp.com/api/random/category_${buttonText}`)
            .then((response) => {
                loading.style.display = 'none';
                result.style.display = 'block';
                const emoji = response.data.htmlCode[0]
                result.textContent = emoji
            });
    } catch (error) {
        console.log(error);
        loading.style.display = 'none';
        result.style.display = 'none';
        errors.textContent = 'Sorry, something went wrong.';
    }
};

some-failure-emoji-view

あれ?

HTMLを見てみると、以下のようになっていました。

<!-- 表示されていたHTML -->
<p class="emoji" style="display: block;">&#129409;</p>

<!-- 実際のコード(HTMLとして編集を選択) -->
<p class="emoji" style="display: block;">&amp;#129409;</p>

状況の整理

APIから返ってくる値

  • HTMLCode数値文字参照

    • 数値文字参照 表記したい文字をUnicode/ISO 10646の文字番号(コードポイント)で表す方式で、十進数を用いる場合は「&#番号;」、16進数を用いる場合は「&#x番号;」のように表記する。
    • 文字実体参照 MDN - エンティティ 仕組みは数値文字参照と一緒。予め言語仕様で定義された文字が使える。
  • unicode → 絵文字に割り当てられたunicode

HTMLで絵文字表示 HTMLで絵文字コード表示 JSから絵文字挿入 JSから絵文字コード挿入
入力 🐩 &#128041; 🐩 &#128041;
出力 🐩 🐩 🐩 &#128041;

JSから絵文字コードを挿入すると、 絵文字コード⇔絵文字の変換が行われず、& がHTMLに挿入されるときに &amp:(文字実体参照)に変換されるみたいですね。

解決策 UniCode文字列を絵文字に変換する。

絵文字そのままINが問題ないなら、そもそも絵文字に変換してから挿入すればいいのでは?

U+は16進数を表すらしいので、 U+1F33D → 0x1F33D かな?

> String.fromCharCode(0x1F33D);
''

・・・?

サロゲートペア

参考記事 - Qiita

Unicode番号が16進数で10000以上の文字を、UTF-8(およびUTF-16)では表現できないため、Unicode番号D800DBFFDC00DFFFの組み合わせで表現した仕組み。

要するに、実際は組み合わせで表されてるよ〜ということらしい。

fromCodePoint というメソッドを使えば解決できそう。

> String.fromCodePoint(0x1F33D);
'🌽'

あとは U+0xに置換すればOKですね。

emojiUnicode =  emojiUnicode.replace("U+", "0x");

MDN - replace

replace の返り値は文字列なのですが、 そのままString.fromCodePoint に渡しても普通に動作しました。 String.fromCodePoint の引数には codepointsとしか書かれていないので、そういうものなのでしょうか。

最後にボタンを押したら絵文字をクリップボードにコピー出来るようにします。
ボタン押下のアニメーション等は付けていませんが、まぁ良いでしょう。

クリックしたらコピー 参考

// クリップボードに絵文字をコピー
const emoji = result.textContent;
navigator.clipboard.writeText(emoji);

完成!!

emoji-demo

最終的なコード

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>My Carbon Trigger</title>
        <link rel="stylesheet" href="./styles.css" />
    </head>

    <body class="container">
        <img alt="emoji" src="images/emoji.jpeg" />
        <div>
            <h1>Welcome to Emoji app!</h1>
        </div>
        <div>
            <h2>To get random emojis 🎲 </h2>
        </div>
        <div class="btn-field">
            <button class="search-btn">smileys_and_people</button>
            <button class="search-btn">animals_and_nature</button>
            <button class="search-btn">food_and_drink</button>
            <button class="search-btn">travel_and_places</button>
            <button class="search-btn">activities</button>
            <button class="search-btn">objects</button>
            <button class="search-btn">symbols</button>
            <button class="search-btn">flags</button>
        </div>

        <div class="result">
            <div class="loading">loading...</div>
            <div class="errors"></div>
            <div class="data"></div>
            <div role="img" aria-label="emoji">
                <p><strong>Emoji</strong></p>
                <p class="emoji"></p>
            </div>
        </div>

        <button id="copy">Copy!</button>

        <script src="main.js"></script>
    </body>
</html>
import axios from 'axios';

// ボタン
const searchButtons = document.getElementsByClassName("search-btn");
const copyButton = document.getElementById("copy");
// リザルト
const errors = document.querySelector('.errors');
const loading = document.querySelector('.loading');
const result = document.querySelector('.emoji');

const clickSearch = e => {
    // イベントからボタンのテキストを取得
    const buttonText = e.target.textContent;
    getRandomEmoji(buttonText);
}

const clickCopy = e => {
    // クリップボードに絵文字をコピー
    const emoji = result.textContent;
    navigator.clipboard.writeText(emoji);
}

const getRandomEmoji = async (buttonText) => {
    try {
        await axios
            .get(`https://emojihub.herokuapp.com/api/random/category_${buttonText}`)
            .then((response) => {
                loading.style.display = 'none';
                result.style.display = 'block';
                let emojiUnicode = response.data.unicode[0];
                // unicode の U+ → 16進数リテラルに変換
                emojiUnicode =  emojiUnicode.replace("U+", "0x");
                // unicode → 絵文字へ変換
                const emoji = String.fromCodePoint(emojiUnicode);
                result.textContent = emoji;
            });
    } catch (error) {
        console.log(error);
        loading.style.display = 'none';
        result.style.display = 'none';
        errors.textContent = 'Sorry, something went wrong.';
    }
};

Array.prototype.forEach.call(searchButtons, button => {
    button.addEventListener('click', clickSearch);
});

copyButton.addEventListener("click", clickCopy);

せっかくなのでGithubに上げる

練習も兼ねてGithubに上げる&READMEを書きました。
READMEに動画を載せたかったので下の記事を参考にしました。

READMEに動画を載せる
https://github.com/NasuPanda/emoji-app

バックグラウンドタスクとパフォーマンス

  • バックグラウンドタスク
    • 拡張機能のアイコンの色を更新する
  • パフォーマンスの改善について

パフォーマンス

開発者ツールから、パフォーマンスに関する情報を収集してみる

performance

わかりやすいのはここ。

performance-graf

どの処理が表示速度低下に繋がっているのかを可視化できる、という感じ?
パフォーマンスに関しては正直余りイメージが沸かない。ので、今後やるべき時になったらやる。

参考になりそう

パフォーマンス向上に対する施策は大別すると以下の2通り
軽量化 (単純にやりとりするデータ容量を小さくすること)
・ 圧縮
・ 削除
最適化 (その時に最も適している実装・実行をとること)
・ 経路・順番の変更
・ 非同期
もっとも遅くしている原因を探して、それを対策するのが原則。「対効果」が絶対的正義である。手段から入るのは愚策。まず先に原因を知ることが重要。

トピック

  • サイトパフォーマンスを正確に把握したい場合キャッシュをクリアすると良い
  • 近年Webでは「重たい」アセット(画像など)を扱うことが増えている
    • 画像が最適化され、ユーザにとって適切なサイズ・解像度になっていると良い
  • HTML/CSS/JSを最適化しよう
    • ブラウザはHTMLに忠実にDOMを構築するので、使用するタグを最小限に抑えると良い
    • 1つのページでしか使わないスタイルはメインページのCSSに含まないようにしよう
    • JSが読み込まれるタイミングに注意
      • 場合によっては defer を検討
  • Site Speed Test の Web サイトでサイトパフォーマンスを測る事ができる

バックグラウンドタスクの実装 色を計算する関数の追加

function calculateColor(value) {
    let co2Scale = [0, 150, 600, 750, 800];
    let colors = ['#2AA364', '#F5EB4D', '#9E4229', '#381D02', '#381D02'];

    let closestNum = co2Scale.sort((a, b) => {
        return Math.abs(a - value) - Math.abs(b - value);
    })[0];
    console.log(value + ' is closest to ' + closestNum);
    let num = (element) => element > closestNum;
    let scaleIndex = co2Scale.findIndex(num);

    let closestColor = colors[scaleIndex];
    console.log(scaleIndex, closestColor);

    chrome.runtime.sendMessage({ action: 'updateIcon', value: { color: closestColor } });
}

sort

    let closestNum = co2Scale.sort((a, b) => {
        return Math.abs(a - value) - Math.abs(b - value);
    })[0];
  • MDN - sort
  • 比較関数を渡すと、その結果に応じてソートできる
    • 通常は a-b のように普通の計算式を使う
  • a, bを使った計算で値が負の場合は a をより小さいインデックスに移動する
    • つまり a を前にソートする
  • 例えば40と100を比べる時は compare(40,100)となり、関数は40-100=-60(負の値)を返す
    • 40のほうが小さいため前にソートされる
  • 結果が正の場合はその逆に動く
  • 降順/昇順を入れ替えたければ比較関数の中身を変えればいい

findIndex

let num = (element) => element > closestNum;
let scaleIndex = co2Scale.findIndex(num);

以下のコードと同じ動きをする。

let scaleIndex = co2Scale.findIndex( e => e > closestNum )
  • MDN - findIndex
  • JavaScriptの findIndex は関数を受け取り、指定のテスト関数に合格する最初の要素を返す。
    • 今回の場合 value が300だとすると closestNum は150なので返ってくるインデックスは 2

chrome.runtime

chrome.runtime.sendMessage({ action: 'updateIcon', value: { color: closestColor } });

chrome.runtimeにはさまざまなバックグラウンドタスクを処理するAPIがある。

ここではアイコンの変更のみ行っているが、以下のようなこともできる。

  • バックグラウンドページを取得
  • マニフェストの詳細を返す
  • アプリや拡張機能のライフサイクルでイベントを監視して応答する
  • URL の相対パスを完全修飾 URL に変換する

デフォルトアイコン色設定

chrome.runtime.sendMessage({
    action: 'updateIcon',
        value: {
            color: 'green',
        },
});

関数呼び出し、実行

APIを呼ぶ関数に以下を追加

calculateColor(CO2);

/dist/background.js でバックグラウンドアクションの呼び出し用リスナーを追加

chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
    if (msg.action === 'updateIcon') {
        chrome.browserAction.setIcon({ imageData: drawIcon(msg.value) });
    }
});

function drawIcon(value) {
    let canvas = document.createElement('canvas');
    let context = canvas.getContext('2d');

    context.beginPath();
    context.fillStyle = value.color;
    context.arc(100, 100, 50, 0, 2 * Math.PI);
    context.fill();

    return context.getImageData(50, 50, 100, 100);
}

canvasを使って丸を書いて色を塗っている感じ。(canvasは後で触れるため割愛)

完成!
carbon-trigger

学んだこと

  • フォームの構築
  • ローカルストレージの利用
  • APIの呼び出し、axios概要
  • Promise概要
  • async/await概要
  • APIを使った簡単な拡張機能の構築
    • 文字コードの扱い
  • パフォーマンスの測定、バックグラウンドタスク
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0