- 投稿日:2021-10-13T22:59:34+09:00
バイクで走るタイムリミットをLINE_BotとWeb_APIを組み合わせて実装してみた~
近ごろ私達は~いい感じ♪ これが私の生きる道ですね! 見出しだけでピンきて懐かしさを感じた方はおそらく30代Overでしょう笑 (何故いきなり冒頭に?といった疑問は記事を読み進めるとわかりますので、お付き合いください~) さてさて、 10月からプログラミングを習い始めて毎日勉強中の私ですが、 LINE_BotとWeb_APIを組み合わせて何か試したいと思い、 大好きなバイクを例に作ってみました! これが私の走る道 学生時代からバイクで峠を走るのが大好きで、ヒマさえあれば埼玉県飯能市の自宅から 奥多摩湖(東京都西多摩郡)まで友人と走りに行っています。 しかし、1車線の道が続く田舎道なので日中は渋滞ばかりで気持ちよく走れません(´;ω;`)ウゥゥ 特に朝方と夕方は渋滞がひどく、それなら真夜中に気持ちよく走ろう! ということで、夜が更けてから朝方までひたすら峠を走るということを、 学生時代はよくやっていました(あの頃は若かった、、、) 奥多摩の夜は、空気が澄んでいて星が本当に綺麗なんです( ̄ー ̄) 出典:YAMA HACK 東京でも星が見られる!?奥多摩のおすすめの観察スポットとタイミング これが私の求む物 暖かい時期は日の出が早く、日が昇るにつれて交通量も増えます。 朝の渋滞に巻き込まれる前に帰る!というのが毎回のミッションで、 渋滞を回避するために、日の出・日の入りがわかるSunset and sunrise times APIを利用しました。 奥多摩から自宅まで約40分の道のりなので、 日の出の時間がわかれば家に着く時間を逆算して奥多摩を出発、 渋滞が起きる前には清々しい気持ちで家に着くといった魂胆です笑 LINE_BotとWeb_APIを組み合わせて、奥多摩を出発しなけばならない時間を逆算できるようにします~ 環境 Visual Studio Code v1.60.2 node v16.10.0 npm 7.21.1 Botに没頭、できてホッと。 以下、コードです。 server3.js 'use strict'; // ######################################## // 初期設定など // ######################################## // パッケージを使用します const express = require('express'); const line = require('@line/bot-sdk'); const axios = require('axios'); // ローカル(自分のPC)でサーバーを公開するときのポート番号です const PORT = process.env.PORT || 3000; // Messaging APIで利用するクレデンシャル(秘匿情報)です。 const config = { channelSecret: '作成したBotのチャネルシークレット', channelAccessToken: '作成したBotのチャネルアクセストークン' }; // ########## ▼▼▼ サンプル関数 ▼▼▼ ########## const sampleFunction = async (event) => { // ユーザーメッセージが「日の出」か「日の入り」かどうか if (event.message.text !== '今日のタイムリミットは?') { return client.replyMessage(event.replyToken, { type: 'text', text: '「今日のタイムリミットは?」と話しかけてね' }); } else { // 「リプライ」を使って先に返事しておきます await client.replyMessage(event.replyToken, { type: 'text', text: 'ちょいとお待ちを…' }); let pushText = ''; try { // axiosで日の出日の入り時刻のAPIを叩きます(少し時間がかかる・ブロッキングする) const res = await axios.get('https://api.sunrise-sunset.org/json?lat=35.7772463&lng=138.9766782'); // 取得できるのはUTCなので日本時間(+9時間)になおす const utc_time = res.data.results.sunrise; // '時', '分', '秒 PM' に分割する const tm_split = utc_time.split(':'); // '時' を9時間進めて12時間戻す(13時を過ぎないようにする) const jp_hour = Number(tm_split[0]) + 9 - 12; // '秒 PM' を '秒' だけにする const sec = tm_split[2].split(' ')[0]; // 再構成する const time_string = `${jp_hour}時${tm_split[1]}分${sec}秒`; pushText = `今日のタイムリミットは${time_string}です!渋滞前に帰りましょう!`; } catch (error) { pushText = '検索中にエラーが発生しました。ごめんね。'; // APIからエラーが返ってきたらターミナルに表示する console.error(error); } // 「プッシュ」で後からユーザーに通知します return client.pushMessage(event.source.userId, { type: 'text', text: pushText, }); } }; // ########## ▲▲▲ サンプル関数 ▲▲▲ ########## // ######################################## // LINEサーバーからのWebhookデータを処理する部分 // ######################################## // LINE SDKを初期化します const client = new line.Client(config); // LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます async function handleEvent(event) { // 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } // サンプル関数を実行します return sampleFunction(event); } // ######################################## // Expressによるサーバー部分 // ######################################## // expressを初期化します const app = express(); // HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします app.post('/webhook', line.middleware(config), (req, res) => { // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行 if (req.body.events.length === 0) { res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します(なくてもよい) console.log('検証イベントを受信しました!'); // ターミナルに表示します return; // これより下は実行されません } else { // 通常のメッセージなど … Webhookの中身を確認用にターミナルに表示します console.log('受信しました:', req.body.events); } // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、 // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result)); }); // 最初に決めたポート番号でサーバーをPC内だけに公開します // (環境によってはローカルネットワーク内にも公開されます) app.listen(PORT); console.log(`ポート${PORT}番でExpressサーバーを実行中です…`); LINE_BotとWeb_APIと習ったので、様々なAPIを叩いてみたものの、うまく動かすことが出来ず。 (動かすことができたら記事UPしますね~) そこは引き続き頑張るとして、今回はこんな感じのコードです! 緯度経度を奥多摩湖の場所に変更したり、出力文言を変更したり、 使っているコードを解読しながら、数時間没頭してやっと完成! 結果 「今日のタイムリミットは?」と入力すると、 「ちょいとお待ちを、、、」 「今日のタイムリミットは〇時〇分〇秒です!渋滞前に帰りましょう!」 とリプライメッセージが届きます。 この時間が奥多摩湖の日の出の時間ですね! 適当な文字を入れても質問文字を促してくれるので大丈夫です。 バイク乗りの本音で言うと、朝が冷え込む時期になると手先の感覚皆無で、 スマホ打つどころではなくなるので、実際使う時の入力は「あ」にしたいと思います~! 他にも面白そうなAPIがたくさんありますので、しばらくBotに没頭していきます(´-`).。oO 参考:APIマーケットプレイス - 10,000以上のRest APIにアクセス | Rakuten RapidAPI 参考:APIを探して、試して、使える - APIbank これが私に活きるか未知( ̄ー ̄) うそです、使いこなせるように頑張ります。 最後まで読んでいただき、ありがとうございました!
- 投稿日:2021-10-13T22:47:41+09:00
APIとLINE Botを組み合わせることで、患者さんが必要な情報にアクセスできる仕組みを目指す その1-天気を伝える
患者さんご家族にとって便利なLINE Botを作りたい 福岡で在宅医療を中心とした医療機関を運営しています。 当院の患者さんやご家族、もしくは当院に診察を依頼したいと考えてくださっている方向けに、必要な情報にアクセスできるLINE Botを作ろうと思い取り組んでいる経過です。 環境・利用API node v16.10.0 Visual Studio Code 1.60.2 axios 0.22.0 ngrok 2.3.40 お薬APIは有料のため断念 API連携の方法を学び、在宅医療で役にたちAPIで持って来れる情報に何があるかを考えました。 当初、薬の情報がAPIでもってこれれば、お薬の表面に書いてある文字や、色形で薬が何の薬か判断できるようなり、治療の経過で「血圧の薬だけ明日から抜く」などを患者さんご家族でもやりやすくなるかと考えましたが、薬の情報を使えるAPIは有料のものばかりのようで断念(いいAPIがあれば教えてください)。 お天気APIとの連携 次に考えたのが、お天気APIとの連携です。 病院で入院や外来を担当していたときはあまり天気を気にしていませんでしたが、在宅医療に携わるようになって晴れか雨かはもちろん、夏の暑さや冬の暑さが自分自身にダイレクトに関わるようになりました。 また、患者さんも引きこもりがちな方が多いため散歩を勧めるけど暑すぎたり寒すぎたりする日は難しかったり、特に高齢者は暑い寒いを感じる体のセンサーが弱っていて暑い中でもクーラーをつけずに過ごしすぐに熱中症になる方がいたりと、お天気情報の重要性を感じています。 そこで、天気の情報をAPIでもってきて、その日の天気に合わせてメッセージを送れるようにならないかと考えた次第です。 OpneWeatherMapを使う 天気APIは無料のものがいくつかありますが、今回は最も代表的なOpenWeatherMapを使いました。 使用するのにAPIキーを発行する必要がありますが、これについてはこちらのサイトを参照しました。 最終的にはLINE Botに乗せることが目標ですが、何せ初心者なのでまずはOpenWeatherMapから必要な情報を抜き出せるかを試してみました。 その日の天気や気温に合わせてコメントを出す http://api.openweathermap.org/data/2.5/weather?q=fukuoka&appid={APIアクセスキー}&lang=ja&units=metric 発行したAPIアクセスキーを該当部に入れ、当院は福岡にあるので場所はfukuokaとして指定、摂氏で温度が欲しいので最後をmetricとしています。 すると、 { coord: { lon: 130.4181, lat: 33.6064 }, weather: [ { id: 803, main: "Clouds", description: "曇りがち", icon: "04n" } ], base: "stations", main: { temp: 23.84, feels_like: 23.87, temp_min: 23.44, temp_max: 24.01, pressure: 1020, humidity: 61 }, visibility: 10000, wind: { speed: 3.6, deg: 330 }, clouds: { all: 75 }, dt: 1634121790, sys: { type: 1, id: 7998, country: "JP", sunrise: 1634073664, sunset: 1634114871 }, timezone: 32400, id: 1863967, name: "福岡市", cod: 200 } と、JSONで情報が返ってきます。 この情報を使って、晴れの日には「いいお天気ですよ」、曇りの日には「お散歩はいかがですか」、雨の日には「お出かけの際は傘をお持ちください」と表示し、さらに最高気温が30度以上の場合は「熱中症にご注意ください」、32度以上の場合は「クーラーを入れてください」と表示することに。 コードこちら // axiosライブラリを呼び出す const axios = require('axios'); // 実際にデータを取得する getRequest 関数 async function getRequest() { let response; try { response = await axios.get('http://api.openweathermap.org/data/2.5/weather?q=fukuoka&appid=42d222cdac5c766d1af96314e63f948d&lang=ja&units=metric'); if (response.data.weather[0].main === 'Clouds'){ console.log('お散歩はいかがですか'); } if (response.data.weather[0].main === 'Rain'){ console.log('お出かけの際は傘をお持ちください'); } if (response.data.weather[0].main === 'Clear'){ console.log('いいお天気ですよ'); } if (response.data.main.temp_max > 30){ console.log('熱中症にご注意ください'); } if (response.data.main.temp_max > 32){ console.log('クーラーを入れてください'); } else{ console.log('以上です'); } } catch (error) { console.error(error); } } // getRequest を呼び出してデータを読み込む getRequest(); これで、その日の福岡の天気や気温に合わせた情報が返ってくるようになりました。 LINE Botと連携する 次に、これをLINE Botと連携します。 せっかくなので、上記で試したif文をつかって天気情報によってLINEで返信するものを作りたかったのですが、何度やってもエラーが出てうまくいかず、、 今の力の限界ということで、シンプルに天気を返してくれるものを作りました。 'use strict'; // ######################################## // 初期設定など // ######################################## // パッケージを使用します const express = require('express'); const line = require('@line/bot-sdk'); const axios = require('axios'); // ローカル(自分のPC)でサーバーを公開するときのポート番号です const PORT = process.env.PORT || 3000; // Messaging APIで利用するクレデンシャル(秘匿情報)です。 const config = { channelSecret: '作成したBotのチャネルシークレット', channelAccessToken: '作成したBotのチャネルアクセストークン' }; // ########## ▼▼▼ サンプル関数 ▼▼▼ ########## const sampleFunction = async (event) => { const userText = event.message.text; let replyText = ''; // 部分一致1(「範囲」という文字が1ヶ所でも含まれていたら反応) if (userText.indexOf('範囲') > -1) { replyText = 'https://taro-cl.com/for_patients#b'; } // 部分一致2(「費用」という単語が1ヶ所でも含まれていたら反応) if (userText.indexOf('費用') > -1) { replyText = 'https://taro-cl.com/q_and_a'; } // 部分一致3(「天気」という単語が1ヶ所でも含まれていたら反応) if (userText.indexOf('天気') > -1) { let response = await axios.get('http://api.openweathermap.org/data/2.5/weather?q=fukuoka&appid=42d222cdac5c766d1af96314e63f948d&lang=ja&units=metric'); console.log(response.data.weather[0].main); replyText = '「今日の天気は' + response.data.weather[0].description + 'です」'; } // この時点でどの条件にも引っかかってない(replyTextが空文字列のまま)なら相槌をうっておく if (replyText === '') { replyText = '当院にお問い合わせください(092-410-3333)'; } return client.replyMessage(event.replyToken, { type: 'text', text: replyText }); }; // ########## ▲▲▲ サンプル関数 ▲▲▲ ########## // ######################################## // LINEサーバーからのWebhookデータを処理する部分 // ######################################## // LINE SDKを初期化します const client = new line.Client(config); // LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます async function handleEvent(event) { // 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } // サンプル関数を実行します return sampleFunction(event); } // ######################################## // Expressによるサーバー部分 // ######################################## // expressを初期化します const app = express(); // HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします app.post('/webhook', line.middleware(config), (req, res) => { // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行 if (req.body.events.length === 0) { res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します(なくてもよい) console.log('検証イベントを受信しました!'); // ターミナルに表示します return; // これより下は実行されません } else { // 通常のメッセージなど … Webhookの中身を確認用にターミナルに表示します console.log('受信しました:', req.body.events); } // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、 // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result)); }); // 最初に決めたポート番号でサーバーをPC内だけに公開します // (環境によってはローカルネットワーク内にも公開されます) app.listen(PORT); console.log(`ポート${PORT}番でExpressサーバーを実行中です…`); 天気情報に加えて、尋ねられることが多い「費用」と「訪問範囲」について、それぞれ「費用」「範囲」という言葉が含まれる送信があれば、それぞれを説明しているホームページ部分のリンクを返信するようにしました。 実際に作動している様子がこちらです。 https://youtu.be/FobBOvWumrU なんとか動いてくれました。 自分の学びに合わせてLINE Botもアップデートしていきたいと思います。
- 投稿日:2021-10-13T21:51:33+09:00
WEBストレージ(localStorage, sessionStorage)、cookieをJavaScriptで使ってみる
WEBストレージ(localStorage, sessionStorage)、cookieの概要 WEB Storage(localStorage, sessionStorage)はHTML5で新しく作られた仕組みで、ブラウザ上にデータを保存することができます。同じくデータを保存する仕組みにcookieがありますが、こちらは主にセッションを管理する目的で使用され、単なるブラウザ上のデータの保存はWEB Storageを利用することが多いみたいです。 ブラウザに保存されているデータは開発者ツールのアプリケーションタブで見ることができます。 参考に一覧があるので、見ることをお勧めします。 参考 試してみる localStorageとsessionStorageの使い方は基本的に同じ。 localStorage.setItem(key, 値);でデータを保存して、localStorage.getItem(key)で保存されている値を取得する。また、localStorage.removeItem(key);でkeyに対応する値を削除する。(sessionStorageのメソッドも同じ) index.html ... <script src="./index.js"></script> </head> <body> <h1>local storageを使ってみる</h1> <input type="button" value="localStorage" onclick='setLocalStorage()'> <input type="button" value="sessionStorage" onclick='setSessionStorage()'> <input type="button" value="cookie" onclick='setCookie()'> </body> </html> localStorageを使ってみる index.js let setLocalStorage = () => { if(localStorage.getItem('localKey') === null) { //localStorageにデータがなかったらhogeを入れる localStorage.setItem('localKey', 'hoge'); } else if(localStorage.getItem('localKey') === 'hoge') { //localStorageにデータがあったら'hoge'+'hoge'を入れる let localKeyValue = localStorage.getItem('localKey'); localStorage.setItem('localKey', localKeyValue + 'hoge'); } else { //localStorageを削除する localStorage.removeItem('localKey'); } } ボタンを1回クリックしたとき ボタンを2回クリックしたとき sessionStorageを使ってみる index.js let setSessionStorage = () => { if(sessionStorage.getItem('sessionKey') === null) { sessionStorage.setItem('sessionKey', 'hoge'); } else if(sessionStorage.getItem('sessionKey') === 'hoge') { //sessionStorageにデータがあったら'hoge'+'hoge'を入れる let sessionKeyValue = sessionStorage.getItem('sessionKey'); sessionStorage.setItem('sessionKey', sessionKeyValue + 'hoge'); } else { //sessionStorageを削除する sessionStorage.removeItem('sessionKey'); } } ボタンを2回クリックしたとき こちらはタブを閉じて開きなおすと値が消えていることが確認できる。 cookieを使ってみる。 cookieの場合はストレージとは違い、document.cookie = 'name=hoge';といった形でデータを保存する。複数の値を設定する場合も一つずつ順にdocument.cookieに代入することで、それぞれ別に保存することができる。 cookieの保存期間を設定する場合、document.cookie = "名前=値; max-age=残存期間"; またはdocument.cookie = "名前=値; expires=日付";と記載すればよい。 参考 index.js let setCookie = () => { // console.log(document.cookie) if(document.cookie === '') { document.cookie = 'name=hoge'; } else if(document.cookie === 'name=hoge'){ let preCookie = document.cookie; console.log(document.cookie) document.cookie = `id=1; max-age=300`; //有効期間300秒を設定 } else { console.log(document.cookie); } } ボタンを2回クリックする ボタンを3回クリック 以下のように名前=値;名前=値;の形式で保存されている ブラウザを再起動すると有効期間を設定したidのみ保存されていることが確認できる。
- 投稿日:2021-10-13T18:55:20+09:00
事故を起こさないために、日の入りと天気を教えてくれるLineBotをつくってみた
プログラミング初心者がLineBotをつくってみた 飲食店で働く私が、学んだプログラミングをもとに、LineBotを作成してみました。 こんな風に考えたらできた!というところや、つまづいた箇所もまとめたので、 そこも含めて、読んでいただければと思います。 コロナ禍でのデリバリー需要の増加 コロナ禍になり、UberEatsや出前館などのデリバリーをよく見るようになりました。 飲食店からすると、この委託配送ってすごく手数料が取られて、大変なんです。 ということで、自分たちで運んじゃえ!と思い、 私の働くお店でもデリバリーを始めることになりました。 事故の傾向 そうしていく中で、手探りの中でやっていたのですが、 悲しいことに、事故が頻発してしまいました。 事故が起きやすい原因の傾向が、 ・夕暮れ時でライトを点けずに、車に見落とされる ・雨の日にスリップして転倒する の2つがあります。 なので、事故を未然に防ぐために、 ①日の入り時刻が分かるLineBot ②今日の天気が分かるLineBot をつくろうと思いました。 事前準備 まずは、LineBotのアカウントをつくったりと準備が必要なのですが、 今日は割愛します。 1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest こちらの記事の作ってみよう!のところが分かりやすいので、参考にご準備ください。 共通テンプレート 下記のコードをもとに、サンプル関数の部分を変えて、作成していきました。 server.js 'use strict'; // ######################################## // 初期設定など // ######################################## // パッケージを使用します const express = require('express'); const line = require('@line/bot-sdk'); const axios = require('axios'); // ローカル(自分のPC)でサーバーを公開するときのポート番号です const PORT = process.env.PORT || 3000; // Messaging APIで利用するクレデンシャル(秘匿情報)です。 const config = { channelSecret: '作成したBotのチャネルシークレット', channelAccessToken: '作成したBotのチャネルアクセストークン' }; // ########## ▼▼▼ サンプル関数 ▼▼▼ ########## (この行をサンプル関数丸ごと全部と置き換えてね) // ########## ▲▲▲ サンプル関数 ▲▲▲ ########## // ######################################## // LINEサーバーからのWebhookデータを処理する部分 // ######################################## // LINE SDKを初期化します const client = new line.Client(config); // LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます async function handleEvent(event) { // 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } // サンプル関数を実行します return sampleFunction(event); } // ######################################## // Expressによるサーバー部分 // ######################################## // expressを初期化します const app = express(); // HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします app.post('/webhook', line.middleware(config), (req, res) => { // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行 if (req.body.events.length === 0) { res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します(なくてもよい) console.log('検証イベントを受信しました!'); // ターミナルに表示します return; // これより下は実行されません } else { // 通常のメッセージなど … Webhookの中身を確認用にターミナルに表示します console.log('受信しました:', req.body.events); } // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、 // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result)); }); // 最初に決めたポート番号でサーバーをPC内だけに公開します // (環境によってはローカルネットワーク内にも公開されます) app.listen(PORT); console.log(`ポート${PORT}番でExpressサーバーを実行中です…`); ①日の入り時刻が分かるLineBot 最初に教えてもらったコードが大阪府北区の日の出を調べるコードでした。 Sunset and sunrise times APIを利用しています。 日の出を調べる server2.js const sampleFunction = async (event) => { // ユーザーメッセージが「日の出」か「日の入り」かどうか if (event.message.text !== '今日の日の出は?') { return client.replyMessage(event.replyToken, { type: 'text', text: '「今日の日の出は?」と話しかけてね' }); } else { // 「リプライ」を使って先に返事しておきます await client.replyMessage(event.replyToken, { type: 'text', text: '調べています……' }); let pushText = ''; try { // axiosで日の出日の入り時刻のAPIを叩きます(少し時間がかかる・ブロッキングする) const res = await axios.get('https://api.sunrise-sunset.org/json?lat=34.6959484&lng=135.4927352'); // 取得できるのはUTCなので日本時間(+9時間)になおす const utc_time = res.data.results.sunrise; // '時', '分', '秒 PM' に分割する const tm_split = utc_time.split(':'); // '時' を9時間進めて12時間戻す(13時を過ぎないようにする) const jp_hour = Number(tm_split[0]) + 9 - 12; // '秒 PM' を '秒' だけにする const sec = tm_split[2].split(' ')[0]; // 再構成する const time_string = `${jp_hour}時${tm_split[1]}分${sec}秒`; pushText = `今日の大阪市北区付近の日の出は${time_string}です!`; } catch (error) { pushText = '検索中にエラーが発生しました。ごめんね。'; // APIからエラーが返ってきたらターミナルに表示する console.error(error); } // 「プッシュ」で後からユーザーに通知します return client.pushMessage(event.source.userId, { type: 'text', text: pushText, }); } }; ちょっと工夫 このコードを見て、 1)`lat=34.6959484&lng=135.4927352'`の緯度と経度変えれば行けるんじゃないか? 2)`日の出`の部分は全部`日の入り`に変えちゃえ! 3)`const utc_time = res.data.results.sunrise;`のsunriseを`sunset`に変えたら行けるんじゃないか? と思い、書き換えたコードがこちら! 日の入りを調べる server3.js const sampleFunction = async (event) => { // ユーザーメッセージが「日の出」か「日の入り」かどうか if (event.message.text !== '今日の日の入りは?') { return client.replyMessage(event.replyToken, { type: 'text', text: '「今日の日の入りは?」と話しかけてね' }); } else { // 「リプライ」を使って先に返事しておきます await client.replyMessage(event.replyToken, { type: 'text', text: '調べています……' }); let pushText = ''; try { // axiosで日の出日の入り時刻のAPIを叩きます(少し時間がかかる・ブロッキングする) const res = await axios.get('https://api.sunrise-sunset.org/json?lat=35.536325&lng=139.635307'); // 取得できるのはUTCなので日本時間(+9時間)になおす const utc_time = res.data.results.sunset; // '時', '分', '秒 PM' に分割する const tm_split = utc_time.split(':'); // '時' を9時間進めて12時間戻す(13時を過ぎないようにする) const jp_hour = Number(tm_split[0]) + 9 - 12; // '秒 PM' を '秒' だけにする const sec = tm_split[2].split(' ')[0]; // 再構成する const time_string = `${jp_hour}時${tm_split[1]}分${sec}秒`; pushText = `今日の綱島付近の日の入りは${time_string}です!ライトをつけてくださいね!`; } catch (error) { pushText = '検索中にエラーが発生しました。ごめんね。'; // APIからエラーが返ってきたらターミナルに表示する console.error(error); } // 「プッシュ」で後からユーザーに通知します return client.pushMessage(event.source.userId, { type: 'text', text: pushText, }); } }; 結果は・・・ 無事にできました!! ②今日の天気が分かるLineBot こちらの記事を参考に作成しました。 1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest APIはこちらです https://weather.tsukumijima.net/ 天気を調べる server4.js const sampleFunction = async (event) => { if (event.message.text !== '今日の天気は?') { return client.replyMessage(event.replyToken, { type: 'text', text: '「今日の天気は?」と話しかけてね' }); } let replyText = ''; replyText = 'ちょっと待ってね'; //"ちょっと待ってね"ってメッセージだけ先に処理 await client.replyMessage(event.replyToken, { type: 'text', text: replyText }); //axiosを使って天気APIにアクセス const CITY_ID = `140010`; //取得したい地域のIDを指定 const URL = `https://weather.tsukumijima.net/api/forecast?city=${CITY_ID}`; const res = await axios.get(URL); const pushText = res.data.description.text; return client.pushMessage(event.source.userId, { type: 'text', text: pushText, }); } つまづいたところ 1)`CITY_ID`を変えておらず、神奈川県の天気にできなかった。 2)参考記事では最初が`async function handleEvent(event) {`になっていたが、 共通テンプレートでは最後に`return sampleFunction(event);`でサンプル関数を実行しているので、 `const sampleFunction = async (event) => {`で始める必要があった。 そして、結果は・・・ ばっちりできました! 全体を通してつまづいたところ 初心者の私が全体を通してつまづいたところが、 ngrokを立ち上げるたびに、URLが変わるので、webhookは毎回更新する!! ということです。 オウム返しが返ってこないとか、上手く動いてない・・・となったら、まずはここを確認して見てください。 まとめ 同じBot内で日の入りと天気の両方が聞けたらよかったのですが、 結果はバラバラのBotでないと動かない形になりました。 また、位置情報を送ったら、そこから日の入りと天気が返ってくるというところまでできたら、 どんなお店でも使えるので最高だなと思いました。 (まだまだそこまでは行きつかない現実・・・) でも、自分が調べたいことに対して、しっかりとBotで返ってくるので、自分の中では大満足です◎ これからも頑張って、勉強するぞー!!
- 投稿日:2021-10-13T18:37:42+09:00
SlackのステータスをGoogleカレンダーから取得する
はじめに この記事は「GoogleカレンダーとSlackステータスを連携する【GAS】」を参考にしたものです。 背景 ちょっとした心がけですが、お昼休憩に入るときはSlackのステータスを「? おひる」に手動で変更しています。 こうしている理由は、仕事の連絡やメンションをくれた方が「あれ?返事遅いな」と思った際に、ステータスを見て「休憩か」とわかってもらうためです。 でも、結構な頻度で変更し忘れる & 手動でやるのが単純にめんどくさいので、「誰か同じことをうまくやっていないかな」と思い記事を書くに至っています。 うらばなし Google Calendar と Slack を連携させる 実は、Googleカレンダーに予め予定を入れておき、予定の時間になると、Slackのステータスが切り替わる機能を使用することは、現状ではマウスでポチポチやれば簡単にできるはずです。著者も過去に使っていました。 ステータスが「in a meeting」になるだけで、内容は不明。 お昼ごはん中なのか、会議中なのか、外出中なのかは、本人以外はわからない状況。 まぁこれはこれで良くて、どんな会議なのか知られる必要が特にない方にとってはgoodな機能。 しかし、やはり私としては「ご飯中」だ、「会議中」だ、「犬の散歩中」だ、「病院なう」だということを、slackのステータスにどうしても表示させたい!!という個人的な意向もあり、今回GASを使ってやってみるということになりました。 やりたいことのイメージ ざっと箇条書きにするとこんな感じです。 1→2を3の方法で繰り返し続けるというイメージでしょうか(ただし4をオプションでやりたい)。 Googleカレンダーの予定タイトルを取得する 取得したタイトルをSlackのステータスとして表示する 何分かの間隔で繰り返し取得し続け、ステータスを維持・更新・削除する 予定がない場合は指定したtextや絵文字を表示できるようにする 準備する 必要なものは以下の3点だと思います。 ① GoogleカレンダーID ② アクセスTOKEN Slackアプリ作成する Slack API取得する ③ GASコード コード内に必要情報を埋め込む トリガーの設定 ① GoogleカレンダーID Googleカレンダーにアクセスする Slackのステータスに使いたいカレンダーの「設定と共有」を開く 「カレンダーの統合」を開く カレンダーIDをコピーして保存しておく グループのカレンダーだと「@」以降のidがgroup.cakender.google.comになるようですが、気にせずコピーする。 ② アクセスTOKEN まずは、Slack APIを使うためにSlackアプリを作成します。 Slack apiページから「create NewApp」を開く(From scratchでOK) 詳細ページの「OAuth & Permissions」を開き「Scope」へ 「AppName」を適宜記入し、適用する「Workspace」を選択 > 「Create App」へ 「Add an OAuth Scope」をクリックし、”users.profile:write”を追加する 「OAuth Tokens & Redirect URLs」で「Install to Workspace」をクリック 権限リクエストを許可する 取得した「User OAuth Token」をコピーして保存しておく ③ GASコード GASコードはgoogleドライブから簡単に作成できます。 G Suite Developer Hubにアクセスし、「Start Scripting」をクリック もしくは、GoogleDrive画面から「マイドライブ」 > 「その他」 > 「Google App Script」 で作成 Googleアカウントでログインし、「新しいプロジェクト」で新規作成する。 下記に添付しているコードを貼付け、保存する。(他記事からの引用になります。ほとんど内容は同じですが下記に一応貼っておきます。) 引用元:GoogleカレンダーとSlackステータスを連携する【GAS】 の 「GASコード」部分です。 SlackStatus_Writer.gs /******************************************* その1:Googleカレンダー予定 → Slackステータス用に整形し返す *********************************************/ function createStatusText(event) { // 整形した開始時刻・終了時刻 var start = event.getStartTime().getHours() + ":" + ("00" + event.getStartTime().getMinutes()).slice(-2); var end = event.getEndTime().getHours() + ":" + ("00" + event.getEndTime().getMinutes()).slice(-2); // ステータステキスト var text = event.getTitle() + "(" + start + "から" + end + ")"; // イベントがある時のステータス var event_status = { "profile": JSON.stringify({ "status_text": text, "status_emoji": ":すきなアイコンのコマンドを入力する:" }) }; return event_status; } /******************************************* その2:作成済ステータス → SlackWebAPI経由 → プロフィールへ *******************************************/ function postSlackStatus(status) { // アクセス情報 const TOKEN = "準備の②で取得したUser OAuth Tokenをここに"; const URL = "https://slack.com/api/users.profile.set"; // HTTPヘッダー const headers = { "Authorization" : "Bearer " + TOKEN }; //POSTデータ var option = { "Content-Type": "application/json", "headers": headers, "method": "POST", "payload": status }; var fetch = UrlFetchApp.fetch(URL, option); } /******************************************* その3:カレンダーの予定取得 → Slackステータスを更新 *******************************************/ function main() { // カレンダーID const ID = "準備の①で取得したカレンダーIDはここ"; // 今日の日付 var date = new Date(); // カレンダーから今日の予定を取得 var calendar = CalendarApp.getCalendarById(ID); var events = calendar.getEventsForDay(date); // 今日のイベントがない場合は何もしない if (events.length !== 0) { の // イベントがないときのステータス var set_status = { "profile": JSON.stringify({ "status_text": "Free time!", "status_emoji": ":dancer:" }) }; // 今日の予定をすべて調査 for (var i in events){ // 終日の予定の場合はスルー if (events[i].isAllDayEvent()) { continue; } // 今が予定の開始時刻以降で終了時刻以前なら今はその予定の最中 -> ステータス変更 if (events[i].getStartTime() <= date && events[i].getEndTime() >= date) { set_status = createStatusText(events[i]); break; } } postSlackStatus(set_status); } } コード手直し その1〜その3のブロック(関数)で成り立っているコードになっています。 各自準備してもらった内容を記入したり、表示内容を決めてもらう必要がある箇所は、以下の通りです。 その1箇所:予定がある際、Slackのステータのスアイコンを決める - "status_emoji": ":すきなアイコンのコマンドを入力する:" + "status_emoji": ":calender:" その2箇所:準備②で保存したTOKENを貼付する const ID = "準備の①で取得したカレンダーIDはここ"; その2箇所:Googleカレンダーに予定がない際のアイコンとメッセージを決める "status_text": "Free time!", "status_emoji": ":dancer:" その3箇所:GoogleカレンダーIDを貼付する const ID = "準備の①で取得したカレンダーIDはここ"; コードを動かす さて、コードを動かす準備は整いました。 コード実行時には、どのメソッドを最初に動かすかを指定できます。 必ず、今回はmainを選択してから実行するようにしましょう。 また、初回のみGoogleカレンダーへのアクセスの確認が必要なため、その設定も済ませてください。 うまく動けば、画面下部分の「実行ログ」に開始と終了のお知らせが表示されるはずです。 定期実行の設定をする ここまでこれば、あとは定期実行させる設定をするのみです。 画面の左部分から時計のマーク「トリガー」をクリックする 「トリガーを追加」をクリックする 赤枠部分の設定を行い保存をクリックする 赤枠以外の部分はお好みでどうそ こうすることで、指定した時間間隔でGASが定期実行されます。 完成すると カレンダーで作成した予定がSlackのステータスに反映されていることが確認できるはず。
- 投稿日:2021-10-13T18:37:42+09:00
[非エンジニアでもできる] SlackのステータスをGoogleカレンダーから取得する
はじめに この記事は「GoogleカレンダーとSlackステータスを連携する【GAS】」を参考にしたものです。 背景 ちょっとした心がけですが、お昼休憩に入るときはSlackのステータスを「? おひる」に手動で変更しています。 こうしている理由は、仕事の連絡やメンションをくれた方が「あれ?返事遅いな」と思った際に、ステータスを見て「休憩か」とわかってもらうためです。 でも、結構な頻度で変更し忘れる & 手動でやるのが単純にめんどくさいので、「誰か同じことをうまくやっていないかな」と思い記事を書くに至っています。 うらばなし Google Calendar と Slack を連携させる 実は、Googleカレンダーに予め予定を入れておき、予定の時間になると、Slackのステータスが切り替わる機能を使用することは、現状ではマウスでポチポチやれば簡単にできるはずです。著者も過去に使っていました。 ステータスが「in a meeting」になるだけで、内容は不明。 お昼ごはん中なのか、会議中なのか、外出中なのかは、本人以外はわからない状況。 まぁこれはこれで良くて、どんな会議なのか知られる必要が特にない方にとってはgoodな機能。 しかし、やはり私としては「ご飯中」だ、「会議中」だ、「犬の散歩中」だ、「病院なう」だということを、slackのステータスにどうしても表示させたい!!という個人的な意向もあり、今回GASを使ってやってみるということになりました。 やりたいことのイメージ ざっと箇条書きにするとこんな感じです。 1→2を3の方法で繰り返し続けるというイメージでしょうか(ただし4をオプションでやりたい)。 Googleカレンダーの予定タイトルを取得する 取得したタイトルをSlackのステータスとして表示する 何分かの間隔で繰り返し取得し続け、ステータスを維持・更新・削除する 予定がない場合は指定したtextや絵文字を表示できるようにする 準備する 必要なものは以下の3点だと思います。 ① GoogleカレンダーID ② アクセスTOKEN Slackアプリ作成する Slack API取得する ③ GASコード コード内に必要情報を埋め込む トリガーの設定 ① GoogleカレンダーID Googleカレンダーにアクセスする Slackのステータスに使いたいカレンダーの「設定と共有」を開く 「カレンダーの統合」を開く カレンダーIDをコピーして保存しておく グループのカレンダーだと「@」以降のidがgroup.cakender.google.comになるようですが、気にせずコピーする。 ② アクセスTOKEN まずは、Slack APIを使うためにSlackアプリを作成します。 Slack apiページから「create NewApp」を開く(From scratchでOK) 詳細ページの「OAuth & Permissions」を開き「Scope」へ 「AppName」を適宜記入し、適用する「Workspace」を選択 > 「Create App」へ 「Add an OAuth Scope」をクリックし、”users.profile:write”を追加する 「OAuth Tokens & Redirect URLs」で「Install to Workspace」をクリック 権限リクエストを許可する 取得した「User OAuth Token」をコピーして保存しておく ③ GASコード GASコードはgoogleドライブから簡単に作成できます。 G Suite Developer Hubにアクセスし、「Start Scripting」をクリック もしくは、GoogleDrive画面から「マイドライブ」 > 「その他」 > 「Google App Script」 で作成 Googleアカウントでログインし、「新しいプロジェクト」で新規作成する。 下記に添付しているコードを貼付け、保存する。(他記事からの引用になります。ほとんど内容は同じですが下記に一応貼っておきます。) 引用元:GoogleカレンダーとSlackステータスを連携する【GAS】 の 「GASコード」部分です。 SlackStatus_Writer.gs /******************************************* その1:Googleカレンダー予定 → Slackステータス用に整形し返す *********************************************/ function createStatusText(event) { // 整形した開始時刻・終了時刻 var start = event.getStartTime().getHours() + ":" + ("00" + event.getStartTime().getMinutes()).slice(-2); var end = event.getEndTime().getHours() + ":" + ("00" + event.getEndTime().getMinutes()).slice(-2); // ステータステキスト var text = event.getTitle() + "(" + start + "から" + end + ")"; // イベントがある時のステータス var event_status = { "profile": JSON.stringify({ "status_text": text, "status_emoji": ":すきなアイコンのコマンドを入力する:" }) }; return event_status; } /******************************************* その2:作成済ステータス → SlackWebAPI経由 → プロフィールへ *******************************************/ function postSlackStatus(status) { // アクセス情報 const TOKEN = "準備の②で取得したUser OAuth Tokenをここに"; const URL = "https://slack.com/api/users.profile.set"; // HTTPヘッダー const headers = { "Authorization" : "Bearer " + TOKEN }; //POSTデータ var option = { "Content-Type": "application/json", "headers": headers, "method": "POST", "payload": status }; var fetch = UrlFetchApp.fetch(URL, option); } /******************************************* その3:カレンダーの予定取得 → Slackステータスを更新 *******************************************/ function main() { // カレンダーID const ID = "準備の①で取得したカレンダーIDはここ"; // 今日の日付 var date = new Date(); // カレンダーから今日の予定を取得 var calendar = CalendarApp.getCalendarById(ID); var events = calendar.getEventsForDay(date); // 今日のイベントがない場合は何もしない if (events.length !== 0) { の // イベントがないときのステータス var set_status = { "profile": JSON.stringify({ "status_text": "Free time!", "status_emoji": ":dancer:" }) }; // 今日の予定をすべて調査 for (var i in events){ // 終日の予定の場合はスルー if (events[i].isAllDayEvent()) { continue; } // 今が予定の開始時刻以降で終了時刻以前なら今はその予定の最中 -> ステータス変更 if (events[i].getStartTime() <= date && events[i].getEndTime() >= date) { set_status = createStatusText(events[i]); break; } } postSlackStatus(set_status); } } コード手直し その1〜その3のブロック(関数)で成り立っているコードになっています。 各自準備してもらった内容を記入したり、表示内容を決めてもらう必要がある箇所は、以下の通りです。 その1箇所:予定がある際、Slackのステータのスアイコンを決める - "status_emoji": ":すきなアイコンのコマンドを入力する:" + "status_emoji": ":calender:" その2箇所:準備②で保存したTOKENを貼付する const ID = "準備の①で取得したカレンダーIDはここ"; その2箇所:Googleカレンダーに予定がない際のアイコンとメッセージを決める "status_text": "Free time!", "status_emoji": ":dancer:" その3箇所:GoogleカレンダーIDを貼付する const ID = "準備の①で取得したカレンダーIDはここ"; コードを動かす さて、コードを動かす準備は整いました。 コード実行時には、どのメソッドを最初に動かすかを指定できます。 必ず、今回はmainを選択してから実行するようにしましょう。 また、初回のみGoogleカレンダーへのアクセスの確認が必要なため、その設定も済ませてください。 うまく動けば、画面下部分の「実行ログ」に開始と終了のお知らせが表示されるはずです。 定期実行の設定をする ここまでこれば、あとは定期実行させる設定をするのみです。 画面の左部分から時計のマーク「トリガー」をクリックする 「トリガーを追加」をクリックする 赤枠部分の設定を行い保存をクリックする 赤枠以外の部分はお好みでどうそ こうすることで、指定した時間間隔でGASが定期実行されます。 完成すると カレンダーで作成した予定がSlackのステータスに反映されていることが確認できるはず。
- 投稿日:2021-10-13T18:06:08+09:00
初心者向けの記事って多すぎん??(javascript初心者向けQiitaAPIでLGTM数TOP10を出してみる)
はじめましての自己紹介 はじめまして!超ウルトラスーパー初心者のぜらちんです。 JavaScript?新しい紅茶?というくらいにチンプンカンプンな30オーバー限界ヲタク女が JavaScriptで何かを作り始めてみる、という記事をこれからいくつか投稿していこうと思います。 何を作ろうか まずはQiitaのAPIを使用して、(この言い回しがあっているかも分からない……。) 初心者が読むべき記事を抽出してみることにします。 そう。自分のために作るのです! Qiitaは記事が多すぎる……。初心者向けだけでも無数にある中から良さそうな記事だけ読みたいんじゃー! 何をもって良い記事とするか。そう。他者からの評価が多い記事だけを読むのです。 作成するものの概要 ・QiitaAPIを使って、記事タイトル、本文、タグに「javascript」と「初心者」が含まれる記事を取得する。 ・2021年9月までの記事を取得することにする。(実行の度に結果がブレるため日付を区切る。) ・結果を手動集計しLGTM数で並び変える。(アナログ・・・!) 環境 Node v16.10.0 axios 0.22.0 完成コード XXXXXは発行されたアクセストークン const axios = require('axios'); //タグに「初心者」「javascript」どちらも設定されている記事 async function main() { for (page = 1;page<=100;page++){ let parm = "https://qiita.com/api/v2/items?page=" + page + "&per_page=100&query=JavaScript+" + encodeURIComponent("初心者") + "+created%3A" + encodeURIComponent("<2021-10"); let response = await axios.get(parm, { headers: { "Authorization": "Bearer XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" } }); //取得したデータ件数分タイトル、URL、LGTM数取得 for (let i = 0; i < response.data.length; i++) { var title_name = response["data"][i]["title"]; var sample = response["data"][i]["url"]; var cntlike = response["data"][i]["likes_count"]; console.log(title_name+"¥"+sample+"¥"+cntlike); } } } main(); コードはこのように記載しましたが、数が多すぎるのか途中でエラーになったので、10ずつ区切って実行し結果をExcelに貼り付けて手動で降順にしました。(圧倒的アナログ……!) 結果を「¥」で結び、Excel上でデータ分割しました。 (タイトルにカンマやスペースを入れいてる記事があったため区切り文字として設定できませんでした。) javascript初心者向けLGTM数TOP10(~2021年9月作成記事) 得られた結果からLGTM数のTOP10を発表します!! それぞれリンクも設定していますので飛んでみてください。 記事タイトル、本文、タグのいずれかに、「javascript」と「初心者」の2語が含まれています。 ※LGTM数は2021/10/13時点のものとなります。 No 記事タイトル LGTM数 1 AtCoder に登録したら次にやること ~ これだけ解けば十分闘える!過去問精選 10 問 ~ 6248 2 イマドキのJavaScriptの書き方2018 5554 3 良いコードの書き方 4363 4 すべての新米フロントエンドエンジニアに読んでほしい50の資料 3876 5 2020年のフロントエンドマスターになりたければこの9プロジェクトを作れ 3647 6 プログラミング勉強を加速させる7つの習慣 3379 7 トップデベロッパーになるために作成したいアプリ8選 3248 8 初心者歓迎!手と目で覚える正規表現入門・その1「さまざまな形式の電話番号を検索しよう」 3163 9 初心者プログラマが犯しがちな過ち25選 2492 10 【保存版・初心者向け】独学でAIエンジニアになりたい人向けのオススメの勉強方法 2444 感想 最後は手動でゴリ押ししたところもあったので、そこが悔やまれました。 情報を取れるだけとってExcelに貼り付けてソートして……と不毛な作業をしていたように思います。 終わり良ければ総て良し!なのかもしれませんが、なんだかスッキリしません。 ですが、今まで使ってこなかった脳みそを使えたので良い勉強になりました! 私のこの記事も抽出期間を広げれば対象に入ってきますが、上位10記事くらいLGTMを付けられる日はおそらく来ないので、今後記事を更新することはなさそうです。 参考にした記事は一番下にまとめていますのでご参照ください。 紆余曲折 Visual Studio Codeを使用してjavascriptでやりました。 初心者の勉強用投稿なのでやってみてダメだったコードも書いていきます。 ここからはとにかく長くなってしまったので、お時間のある方のみどうぞ……。 初心者向けの記事数を得る まずは、タグに「初心者」と設定されている記事数を取得してみます。 const axios = require('axios'); //タグが「初心者」である記事 async function main() { let response = await axios.get('https://qiita.com/api/v2/tags/' + encodeURIComponent('初心者')); console.log(response.data); } main(); こんな結果が得られました。 { followers_count: 5331, icon_url: 'https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/2383c6450ddfcedeb18a168a1a13023ee25687c5/medium.jpg?1611635876', id: '初心者', items_count: 19206 } 記事数が19206件もある! こんなに読み切れない!! 初心者が入口で躓くのは参考資料の多さが原因の一つだと私は思っています。 フォロー数は5331。 そりゃ、継続していけば初心者ではなくなるからこれは納得。 閑話休題。 私が知りたいのはJavaScriptに関する初心者向け記事なので数を絞ってみます。 javascriptの初心者向け記事を取得する 初心者タグが設定されている、かつ、javascriptタグも設定されている記事に絞ればいいのだから、 タグのAND検索でいけそうです。 検索窓に「javascript 初心者」と入れるイメージです。 const axios = require('axios'); //タグに「初心者」「javascript」どちらも設定されている記事 async function main() { let response = await axios.get('https://qiita.com/api/v2/tags/' + encodeURIComponent('初心者') + '&tags/' + + encodeURIComponent('javascript')); console.log(response.data); } main(); 結果は……言うまでもなく失敗です。 そもそも「javascript」は日本語じゃないんだからエンコードする必要がないです。 投稿主は超のつく初心者なので、どうか暖かい目で見てください……。(褒めて伸びるタイプです。) ここでチームに相談!15分調べてわからなかったら聞くに限ります! 調べても分からなかったんだし! 教えてもらった内容で再挑戦します。 const axios = require('axios'); //タグに「初心者」「javascript」どちらも設定されている記事 async function main() { let response = await axios.get('https://qiita.com/api/v2/items?query=tag%3AJavaScript+tag%3A' + encodeURIComponent('初心者') ); console.log(response.data); } main(); エラーは出なくなりました。 成功ですが、なんだか出力された内容が少ないように見えます。 気になるので取得できた記事数を確認してみます。 取得した記事件数を確認 「items_count」を追加して取得した記事件数が確認できるか試してみます。 const axios = require('axios'); //タグに「初心者」「javascript」どちらも設定されている記事 async function main() { let response = await axios.get('https://qiita.com/api/v2/items?query=tag%3AJavaScript+tag%3A' + encodeURIComponent('初心者') ); console.log(response.data.items_count); } main(); 結果は「undefined」という表示。 「undefined」は「未定義」という意味らしい……。 んんーー??どういうことだー?? おそらく「値がない」ということなんだろうと思います。 この方法ではダメなので他の方法を探します。 色んな記事を見てみると「length」で数が取れそうです! 早速書き書き……。 const axios = require('axios'); //タグに「初心者」「javascript」どちらも設定されている記事 async function main() { let response = await axios.get('https://qiita.com/api/v2/items?query=tag%3AJavaScript+tag%3A' + encodeURIComponent('初心者') ); //取得した記事数 console.log(response.data.length); } main(); 結果は「20」と出ました! ……いや!そんなわけない!初心者向けのjavascriptの記事が20件なわけがない!! またしても問題にぶちあたりました……。 記事をさらに取得する Qiita API v2(ページネーション)を調べてみると以下の記載がありました。 per_pageの初期値は20、per_pageの最大値は100に設定されています。 「per_page」をいじる必要がありそうです。 「per_page」の最大値を設定してリトライしてみます。 const axios = require('axios'); //タグに「初心者」「javascript」どちらも設定されている記事 async function main() { let response = await axios.get('https://qiita.com/api/v2/items?per_page=100&query=tag%3AJavaScript+tag%3A' + encodeURIComponent('初心者') ); //取得した記事数 console.log(response.data.length); } main(); 結果は『100』と表示されました!成功です! 最大値に100を設定しているから当たり前ですね……。 そして条件にあてはまる記事が100件なわけないです……。 次はすべての記事を取得する必要があります。 すべての記事を取得する 2021/10/11現在、Qiitaの検索窓で「javascript 初心者」と入力すると、すべての検索結果は「7276」件となりました。 「page」の最大値も設定して取得できる記事の数が増えるか試してみます。 const axios = require('axios'); //タグに「初心者」「javascript」どちらも設定されている記事 async function main() { let response = await axios.get('https://qiita.com/api/v2/items?per_page=100&page=100&query=tag%3AJavaScript+tag%3A' + encodeURIComponent('初心者') ); //取得した記事数 console.log(response.data.length); } main(); 結果は『0』 ええ!なんで!?これは再度調査が必要そうです……。 チームからの助言をまるっと引用 Qiita APIでは検索結果が複数返ってくるようなAPIに対して、それを一度に返すことはせずに、ページという単位で分割して返すようになっています。 この時に使うオプションがper_pageとpageで、per_pageで1ページ辺りに幾つの検索結果を含むかを指定でき、pageでその中の何ページ目を取得するかを指定します。 per_page=100&page=100 という記載をした場合、検索結果を100個ずつに区切って、その中の100番目のページを取得する(=9901番目~10000番目の結果を取得する)という意味になります。 なので、検索結果が9901件以上無いと結果は0になります。 per_page=100 という記載の場合は、検索結果は上と同様に100個ずつに区切られ、その中の1番目のページを取得する(=1番目~100番目の結果を取得する)という意味になります。 ※これはpageというパラメータを指定しなかった場合の初期値が1だからです。ちなみに、per_pageの初期値は20です。 なので、この場合は検索結果が100件以上あれば取得できるデータの個数は100になります。 APIではレスポンスヘッダーというところに要素の合計数が出力されます。 レスポンスのボディ(データ本体)ではなくヘッダーの情報を取りたいときはaxiosで取得した結果に対してheadersという要素にアクセスすると取り出せます。 console.log(response.headers); としていただけるとヘッダーの中身が見れますのでtotal-countという項目を探してみてください。 いただいた助言を元に以下を実行してみます。 const axios = require('axios'); //タグに「初心者」「javascript」どちらも設定されている記事 async function main() { let response = await axios.get('https://qiita.com/api/v2/items?query=tag%3AJavaScript+tag%3A' + encodeURIComponent('初心者') ); //取得した記事数 console.log(response.headers); } main(); 得られた結果の中に『'total-count': '1576',』とありました。 ふむ……。1576件……。思ったより少ないな……。 Qiitaの検索窓で検索した結果は7276件だったので、検索対象をタグではなく、タイトルと本文を含めたすべてにしてみます。 const axios = require('axios'); //タグに「初心者」「javascript」どちらも設定されている記事 async function main() { let response = await axios.get('https://qiita.com/api/v2/items?query=JavaScript+' + encodeURIComponent('初心者') ); //取得した記事数 console.log(response.headers); } main(); 結果は『'total-count': '7280',』(2021/10/13時点) うん!いい感じです! ここから全記事のLGTM数をとり、降順にして上位10個の記事だけを取得します。 記事全文だと長すぎるのでタイトルとURLだけ取得し、末尾にLGTM数も取得するようにしてみます。 for (page = 1;page<=100;page++){ let parm = "https://qiita.com/api/v2/items?page=" + page + "&per_page=100&query=JavaScript+" + encodeURIComponent("初心者") + "+created%3A" + encodeURIComponent("<2021-10"); let response = await axios.get(parm, { headers: { "Authorization": "Bearer a57b66c2c1c2a8a9bb53be4879f6ecf5ab28c5a4" } }); 実行中にまたもやエラー発生! per_page=100&page=100 だと途中でエラーになったので、 per_page=100&page=10 で数回実行し、結果をExcelに貼り付けていくことにします。 本当はコード内だけで降順にしてTOP10の記事だけ抽出、としたかったのですが力量不足でした……。 とりあえずは形にすることができたので自分で自分を褒めてあげたいです!(懐) が、また途中で謎のエラーに引っ掛かりました。 頼れるチームメンバーに相談し、アクセストークンなるものを設定してみました。 ※以下のコードは一部抜粋しています。XXXXX~は発行したアクセストークンを設定。 ※アクセストークンの設定方法は参考記事を参照。 let parm = "https://qiita.com/api/v2/items?page=" + page + "&per_page=100&query=JavaScript+" + encodeURIComponent("初心者") + "+created%3A" + encodeURIComponent("<2021-10"); let response = await axios.get(parm, { headers: { "Authorization": "Bearer XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" } }); これで何とかすべての記事を取得することができました。 最終的に完成したコードは「完成コード」のとおりです。 参考記事 以下の記事を参考にしました。 ▼私にはMarkdownチートシートよりもわかりやすかったです。 Qiita Markdown 書き方 まとめ ▼コメント数を取得するあたりなどまるっとパクった参考にしました。 結局Qiita記事ってどれぐらい書けばいいのさ ▼作成日を指定して取得する箇所を参考にしました。 QiitaAPIで2020年投稿のストック数が多い記事のタグ情報を調べてみた ▼記事をたくさん取得する方法を参考にしました。 「テレワーク」と「新型コロナ」には相関性があるのか確かめてみた。 ▼アクセストークンの設定について参考にしました。 よそ様の情報を簡単に参照できるWEBAPIが本当に簡単なのか体験してみた。 ▼なんやかんや参考にしました。(説明めんどくさくなるな。) Qiita API v2(ページネーション)
- 投稿日:2021-10-13T17:29:40+09:00
ニコ生ゲームの作り方についてまとめた(まとめてくれている)記事 ※リンク多め
ニコ生ゲームとは ニコニコ生放送で視聴者参加型の皆で出来るゲームです。 例:つりっくま、ニコニコタワーなど。 ニコ生ゲームはゲームエンジンとしてAkashic Engineを使用しています。 言語はjavaScriptかTypeScript。 公式HP ニコ生ゲームの作り方 逆引きリファレンス 公式サンプルとか その1 下準備 自分のPCで動作させるためにインストールするもの ※2021/10/13現在の推奨版 その2 Akashic 関連ツールのインストール Windows環境で説明します。 画像の1のところにcmdと入力 画像2のコマンドプロンプトを起動 出てきた黒い画面にこれをコピペしてエンター↓ npm install -g @akashic/akashic-sandbox おわったらこれもコピペしてエンター↓ npm install -g @akashic/akashic-cli その3 テキストエディター いわゆるコードを編集するソフト。 おすすめはVSCode https://azure.microsoft.com/ja-jp/products/visual-studio-code/ Download now もしくは いますぐダウンロード これで下準備は終わり!!(多分) ※コマンドプロンプトのコマンドまとめ cd 作業するフォルダのパス で作業フォルダに移動したら akashic-sandbox でゲーム用のローカルサーバー起動 このURLでゲーム画面確認 http://localhost:3000 CTRL+Cで終了 Y/Nが出た場合はYで終了 ※ Yes と No の意味 画像や音声ファイルを追加するときは終了した状態で akashic scan asset ニコ生ゲーム用に出力する場合は akashic export html --output ./game.zip --atsumaru ※gameのところは任意の名前 ニコ生ゲームの制約 zip ファイルの 展開後の合計サイズが 10 MB 以下 zip ファイルに含まれる game.json が 100KB 以下 全てのテキストファイル (.js, .json, .txt など) の 文字コードが UTF-8 わかりやすい記事 シングルゲームやマルチプレイゲームのサンプルコードや改造のやり方を記事にしてくれています。 僕も大変お世話になっております! 困ったときは・・・(なにかあれば更新予定) Q. IOSで起動しない! A. Audioファイルにaacファイルが含まれてない可能性があります。すべての音声ファイルのoggとaacを入れると解決するかもしれません。 Q. 商品情報の取得に失敗しました。 A. ゲームタイトルの単語ではじかれてるかもしれません。変更してみましょう。 ちなみに僕はこの自作ゲームの 「うんち」という単語を消すと正常にリクエストできるようになりました。 ※推測ですが、「おっぱい」もだめかもしれません ※気を付けたほうがいいかもしれないこと 現状ではニコ生ゲーム上ではキーボード入力がサポートされておらず 強引に対応させると動作しない場合があるみたいです。 ※アツマール上であれば問題ないみたいです。
- 投稿日:2021-10-13T17:16:41+09:00
ニコ生ゲームの作り方についてまとめた(まとめてくれている)記事 ※リンク多め
ニコ生ゲームとは ニコニコ生放送で視聴者参加型の皆で出来るゲームです。 例:つりっくま、ニコニコタワーなど。 ニコ生ゲームはゲームエンジンとしてAkashic Engineを使用しています。 言語はjavaScriptかTypeScript。 公式HP ニコ生ゲームの作り方 逆引きリファレンス 公式サンプルとか その1 下準備 自分のPCで動作させるためにインストールするもの ※2021/10/13現在の推奨版 その2 Akashic 関連ツールのインストール Windows環境で説明します。 画像の1のところにcmdと入力 画像2のコマンドプロンプトを起動 出てきた黒い画面にこれをコピペしてエンター↓ npm install -g @akashic/akashic-sandbox おわったらこれもコピペしてエンター↓ npm install -g @akashic/akashic-cli その3 テキストエディター いわゆるコードを編集するソフト。 おすすめはVSCode https://azure.microsoft.com/ja-jp/products/visual-studio-code/ Download now もしくは いますぐダウンロード これで下準備は終わり!!(多分) ※コマンドプロンプトのコマンドまとめ cd 作業するフォルダのパス で作業フォルダに移動したら akashic-sandbox でゲーム用のローカルサーバー起動 このURLでゲーム画面確認 http://localhost:3000 CTRL+Cで終了 Y/Nが出た場合はYで終了 ※ Yes と No の意味 画像や音声ファイルを追加するときは終了した状態で akashic scan asset ニコ生ゲーム用に出力する場合は akashic export html --output ./game.zip --atsumaru ※gameのところは任意の名前 ニコ生ゲームの制約 zip ファイルの 展開後の合計サイズが 10 MB 以下 zip ファイルに含まれる game.json が 100KB 以下 全てのテキストファイル (.js, .json, .txt など) の 文字コードが UTF-8 わかりやすい記事 シングルゲームやマルチプレイゲームのサンプルコードや改造のやり方を記事にしてくれています。 僕も大変お世話になっております! 困ったときは・・・(なにかあれば更新予定) Q. IOSで起動しない! A. Audioファイルにaacファイルが含まれてない可能性があります。すべての音声ファイルのoggとaacを入れると解決するかもしれません。 Q. 商品情報の取得に失敗しました。 A. ゲームタイトルの単語ではじかれてるかもしれません。変更してみましょう。 ちなみに僕はこの自作ゲームの 「うんち」という単語を消すと正常にリクエストできるようになりました。 ※推測ですが、「おっぱい」もだめかもしれません ※気を付けたほうがいいかもしれないこと 現状ではニコ生ゲーム上ではキーボード入力がサポートされておらず 強引に対応させると動作しない場合があるみたいです。 ※アツマール上であれば問題ないみたいです。
- 投稿日:2021-10-13T17:16:41+09:00
a
ニコ生ゲームとは ニコニコ生放送で視聴者参加型の皆で出来るゲームです。 例:つりっくま、ニコニコタワーなど。 ニコ生ゲームはゲームエンジンとしてAkashic Engineを使用しています。 言語はjavaScriptかTypeScript。 公式HP ニコ生ゲームの作り方 逆引きリファレンス 公式サンプルとか その1 下準備 自分のPCで動作させるためにインストールするもの ※2021/10/13現在の推奨版 その2 Akashic 関連ツールのインストール Windows環境で説明します。 画像の1のところにcmdと入力 画像2のコマンドプロンプトを起動 出てきた黒い画面にこれをコピペしてエンター↓ npm install -g @akashic/akashic-sandbox おわったらこれもコピペしてエンター↓ npm install -g @akashic/akashic-cli その3 テキストエディター いわゆるコードを編集するソフト。 おすすめはVSCode https://azure.microsoft.com/ja-jp/products/visual-studio-code/ Download now もしくは いますぐダウンロード これで下準備は終わり!!(多分) ※コマンドプロンプトのコマンドまとめ cd 作業するフォルダのパス で作業フォルダに移動したら akashic-sandbox でゲーム用のローカルサーバー起動 このURLでゲーム画面確認 http://localhost:3000 CTRL+Cで終了 Y/Nが出た場合はYで終了 ※ Yes と No の意味 画像や音声ファイルを追加するときは終了した状態で akashic scan asset ニコ生ゲーム用に出力する場合は akashic export html --output ./game.zip --atsumaru ※gameのところは任意の名前 ニコ生ゲームの制約 zip ファイルの 展開後の合計サイズが 10 MB 以下 zip ファイルに含まれる game.json が 100KB 以下 全てのテキストファイル (.js, .json, .txt など) の 文字コードが UTF-8 わかりやすい記事 シングルゲームやマルチプレイゲームのサンプルコードや改造のやり方を記事にしてくれています。 僕も大変お世話になっております! 困ったときは・・・(なにかあれば更新予定) Q. IOSで起動しない! A. Audioファイルにaacファイルが含まれてない可能性があります。すべての音声ファイルのoggとaacを入れると解決するかもしれません。 Q. 商品情報の取得に失敗しました。 A. ゲームタイトルの単語ではじかれてるかもしれません。変更してみましょう。 ちなみに僕はこの自作ゲームの 「うんち」という単語を消すと正常にリクエストできるようになりました。 ※推測ですが、「おっぱい」もだめかもしれません ※気を付けたほうがいいかもしれないこと 現状ではニコ生ゲーム上ではキーボード入力がサポートされておらず 強引に対応させると動作しない場合があるみたいです。 ※アツマール上であれば問題ないみたいです。
- 投稿日:2021-10-13T16:24:46+09:00
マッピングとは
エンジニアの間で「マップしといて」「マッピングされてるよ」と話しているのを聞いた。 なんだろと検索してみると、「何かと何かを関連づけること」と出てきた。 元々は「地図を描く」という意味だが、「位置付ける、関連づける」という意味合いもあるようだ。 エンジニア同士では「関連づける」という意味で使われている。 例えば、Linuxのディレクトリで、「/etc」と「/private/etc」がある。 どちらも同じファイルが入っていた。これは2つがマッピングされているということだ。 システム開発でも多く使われているよう。
- 投稿日:2021-10-13T15:22:12+09:00
「X-tech」は時代の寵児なのか
まえおき 最近「X-tech」って無限増殖してると思いませんか? ちょっと検索するだけで色々記事出てきますよね。 【2021年最新】XTechとは?最新の事例と共に徹底解説! 全22種類のX-Tech(〇〇Tech)まとめ 新しい価値を生み出すX-Techとは?世界的に拡大している背景も解説 僕は普段金融機関で勤めてるんですが、流石に「fintech」は一般用語化しています。 新しい技術の恩恵を受けながら、エンジニアは様々な事業領域を日々進歩させているのだなぁと素人ながらに思うわけです。ありがとう、エンジニア。 様々な業界の「ナントカtech」がQiitaの中でどのくらい話題になっているのか APIの練習も兼ねて、タグから投稿数を見てみました。 「X-tech」たちは、そのいずれもが、活発に議論される時代の寵児なのでしょうか? 方法 ①「tech」を含むタグIDは以下を参考にさせていただきました。 Qiitaのタグ一覧(アルファベット順) ② 足りないtechは少し検索して追加しました。 (カッコよくやりたかったんですけどここはアナログ) ③ QiitaAPIは「GET /api/v2/tags」を使用しました。 ソースコード const axios = require("axios"); var tech = ['X-tech','fintech','adtech','civictech','civic_tech','Gregtech','agritech','edtech','GovTech','SalesTech','MediTech','hrtech']; async function main() { for (let i=0; i<tech.length; i++) { let response = await axios.get('https://qiita.com/api/v2/tags/' + tech[i]); console.log(tech[i] + "," +response.data.items_count); } } main(); ちなみに最初に作ったゴリ押しのコードは以下です。その後配列してみました。 ゴリ押しコード async function main() { let response = await axios.get('https://qiita.com/api/v2/tags/' + tech); console.log(tech + "," +response.data.items_count); tech = 'fintech'; response = await axios.get('https://qiita.com/api/v2/tags/' + tech); console.log(tech + "," +response.data.items_count); tech = 'adtech'; response = await axios.get('https://qiita.com/api/v2/tags/' + tech); console.log(tech + "," +response.data.items_count); tech = 'civictech'; response = await axios.get('https://qiita.com/api/v2/tags/' + tech); console.log(tech + "," +response.data.items_count); tech = 'civic_tech'; response = await axios.get('https://qiita.com/api/v2/tags/' + tech); console.log(tech + "," +response.data.items_count); tech = 'Gregtech'; response = await axios.get('https://qiita.com/api/v2/tags/' + tech); console.log(tech + "," +response.data.items_count); tech = 'agritech'; response = await axios.get('https://qiita.com/api/v2/tags/' + tech); console.log(tech + "," +response.data.items_count); tech = 'edtech'; response = await axios.get('https://qiita.com/api/v2/tags/' + tech); console.log(tech + "," +response.data.items_count); tech = 'GovTech'; response = await axios.get('https://qiita.com/api/v2/tags/' + tech); console.log(tech + "," +response.data.items_count); tech = 'SalesTech'; response = await axios.get('https://qiita.com/api/v2/tags/' + tech); console.log(tech + "," +response.data.items_count); tech = 'MediTech'; response = await axios.get('https://qiita.com/api/v2/tags/' + tech); console.log(tech + "," +response.data.items_count); } main(); 結論 やってみてわかったんですが、、、 なんならタグにもなってない「tech」のが多いのですね。笑 結果は「fintech」「civictech」がダントツでした 「edtech」は意外でしたかね。(タグはあまりないんですって) 「X-tech」は名前だけ一人歩きしているのが現状(期待先行)なのかもしれません。 もちろん文中に登場人物として出てくることはあると思いますが、 それじゃあただの村人Aですからね。mobです。 世の中のニーズに合わせてそれぞれの「X-tech」にもっとフォーカスが当たると良いですね 感想 初めてのQiitaAPIでした。 javascriptもぶっちゃけ3日目なので手探り感ハンパない(コーディングも多分変)ですし、分析も「ホントかよ」というところが正直な感想です。 でも、fintechのような名前が知られているものは数字大きく出てますし、タグ名の日本語or英語の条件はみんな同じですから、まったくもって的外れでもないのかなぁと思います。
- 投稿日:2021-10-13T14:46:18+09:00
【JavaScriptのメモ】条件式ifについての注意事項
以下のコードを実行すると、HTMLにどの条件式が実行されるでしょうか? let hour = 14; if (hour > 9) { document.write('Good Morning'); } else if (hour > 11) { document.write('Good Afternoon'); } else if (hour > 17) { document.write('Good Evening); } else if (hour > 22) { document.write('sleepy'); } else { document.write(hour); } 答えは、最初にtrueになるif (hour > 9) です。 【注意事項】 if文は一番早くtrueとなる処理だけ実行するので、後ろのelse ifでも条件が当てはまるからといって処理が実行されるわけでは無い。 つまり、先に実行されるtrue処理のみ表示する。 先のtrueの処理が完了すれば、処理そのものが終了し、以降の条件式は実行されない。
- 投稿日:2021-10-13T13:46:10+09:00
BacklinkoのヘッドレスWordPressとNext.jsへの移行【後半】
SEO事業者はJamstackの潮流を感じています。Googleが次期ページ・エクスペリエンス・アップデートを発表したとき、Backlinkoは自分たちのウェブサイトを改善する必要があることに気が付きました。お役に立てたことを嬉しく思います。 著者:Thom Krupa 2021年3月30日 原文:https://bejamas.io/blog/backlinko-case-study/ 2. 語るのではなく見せる 今後のページ・エクスペリエンス・アップデートでは、パフォーマンスの低下に対処し、UXをBacklinkos社のJamstackへ移行させるのが主な理由です。導入したスタックは、機能を犠牲にすることなくパフォーマンスを向上させることに成功しました。 BejamasのサイトではURLを入力するだけでCore Web Vitalsのスコアを確認することができます。 Bejamas では2020年7月に新しいWebサイトを展開しました。Treo Site Speedツールのおかげで、過去12ヶ月間のデータをチェックでき、さらに直接の競合他社との比較も可能です。 ご覧のとおり、LCP(Largest Contentful Paint)、CLS(Cumulative Layout Shift)、TFB(Time to First Byte)、FID(First Input Delay)が大幅に減少しています。 2-1 読み込み時間 新しいNext.jsと古いWordPressサイトの読み込みを直接比較します。 https://www.science.co.jp/document_jamstack_blog/28677/ どちらのウェブサイトも、3G接続、Chromeブラウザ、WebPageTestによるエミュレートされた低予算のMoto G4モバイルデバイスでテストされています。 新しいウェブサイトの読み込みは3倍速くなりました。しかし、ホームページの改善だけで満足してはいけません。1ページの最適化だけでは、Core Web Vitalsに合格することはできません。オリジンの緑のバッジを表示するには、すべてのページが高速でなければなりません。 3. ユーザーエクスペリエンス向上への道 サーバーのレスポンスが遅ければ、他のことも遅くなります。最初の1バイトまでの時間が改善されましたが、これは他のすべての指標に影響します。この時点で、ロードプロセスはウォーターフォールのようなものであり、あらゆる面で最適化する必要があります。 Netlify Edgeを使うことで、一気にアドバンテージを得ることができました。すべてのファイル(HTMLも!)はCDNから直接提供されます。Next.jsをNetlify上でどのようにしてシームレスに展開したかは、Netlifyのブログ記事に詳しく書かれています。 標準的なサーバーと適切なキャッシュ戦略でも実現できます。しかし、先に述べたように、キャッシングは難しく、それを大規模に行うことはさらに難しくなります。Netlifyはキャッシュの無効化を保証しており、すべてのビルドはデフォルトでグローバルに高速化され、陳腐なコンテンツは一切ありません。しかし、ビルドが終了するまで待つ必要があります。 3-1 最大のコンテンツフル・ペイント LCPを改善するためには、ページのレンダリングを最適化する必要がありました。特にフォントと画像。Backlinkoでは、さまざまなフォントの重さを使用しています。8つのファイルを読み込むのではなく、1つの可変フォントを読み込みます。CrucialもCSSを細かく分割していました。当初、WordPressテーマは100kb以上の圧縮されたブロック化されたCSSを要求していました。 今後、私たちはWebPやAVIFといった最新の画像フォーマットのサポートを追加して、画像をさらに最適化することを計画しています。 3-2 累積レイアウト変更 CLSは、ユーザーの立場からすると非常に悩ましい問題です。ウェブサイトが読み込まれ、最初の要素が表示された後、コンテンツが不意に飛び出してきます。原因としては、フォントの読み込み戦略が悪いことや、クライアントサイドでの条件付きレンダリング(バナーや広告など)が挙げられます。このようなジャンプの原因となるものをすべて修正しました。 3-3 ファーストインプットディレイ リアクトハイドレーションには、性能面でのコストがかかります。TTI(Time to Interactive)やFIDを大幅に向上させることができます。JSの実行が完了する前にWebサイトが出来上がっているように見える。クライアントサイドルーティングのようなインタラクションは、ハイドレーションの部分がないと機能しません。具体的には何が問題なのでしょうか? JSが大量にある場合、特にローエンドのデバイスでは実行に時間がかかります。これを解決するにはいくつかの方法がありますが、最も簡単なのはランタイムJSを削除することです。アンディ・ベルは自分のウェブサイトでこれを行い、素晴らしい結果を残しました。 よく開発者の間では、そのJSのコストに見合うかどうかが議論されます。結局のところ、私たちはほとんどが静的なコンテンツを提供しています。なぜここまでJSを入れる必要があるのか?私たちは、エコシステムとそれに関わる人やツールに賭けます。Next.jsはハイブリッドなアプローチを可能にします。オンリクエストのプレビュー環境を簡単に設定できます。 React.jsのハイドレーションはNext.jsのフレームワークの一部ですが、FIDは同じままです。 3-4 知覚されるパフォーマンス 認識されたパフォーマンスを簡単に測定することはできません。非常に主観的なものです。多くの場合、実験室でのテストではなく、ユーザーがどのようにパフォーマンスを感じるかが重要です。 Backlinkoのサイトではページ間のナビゲーションが瞬時にできます。それは、アセットやコンテンツをプリロードし、クライアントサイドルーティングを使用しているからです。これらの機能はすべてNext.jsに組み込まれています。 3-5 最終的な考え パフォーマンスと機能性のバランスをうまく取ることは、特にBacklinkoのようなウェブサイトでは綱渡りのようなものです。 しかし、技術的なソリューションの中には、それが得意なものもあります。 一番の驚きは、Advanced Custom FieldsとWPGraphQLプラグインの力で、WordPressからデータ構造を大きく変える必要がなかったことです。この2つによって、WordPressは実に堅実で柔軟なヘッドレスCMSとなっています。 また、高速なNext.jsのフロントエンドは、多くのコンテンツ編集者に愛用されているCMSに新たな命を吹き込みます。 WebサイトのUXとスピードを抜本的に改善するという目標は達成されましたが、私たちにとって最も喜ばしい結果は、Jamstackの道を歩むことでパフォーマンス、機能性、セキュリティの間にトレードオフがないことを理解したBacklinkoチームから得られた驚きでした。 最後まで読んで下さり、ありがとうございました Jamstackに関心がある方はこちらまでお問合せください! 株式会社ヒューマンサイエンス https://www.science.co.jp/
- 投稿日:2021-10-13T11:59:35+09:00
JavaScriptでTodoアプリを作る
JavaScript勉強したときにTodoを作ったのでその時のメモです。 完成イメージ フォームに値を入力してTodoを作成、作成したTodoは完了リストに移動か削除できる。 完了リストにあるTodoは未完了のTodoリストに戻すことができる。 というものです。 CSSは最低限しか当ててないので今回は割愛。 実装 HTML <!DOCTYPE html> <html> <head> <title>TODO(JS)</title> <meta charset="UTF-8" /> </head> <body> <div class="input-area"> <input id="add-text" placeholder="TODOを入力" /> <button id="add-button">追加</button> </div> <div class="incomplete-area"> <p class="title">未完了のTODO</p> <ul id="incomplete-list"></ul> </div> <div class="complete-area"> <p class="title">完了したTODO</p> <ul id="complete-list"></ul> </div> <script src="src/index.js"></script> </body> </html> Todo追加 import "./styles.css"; const onClickAdd = () => { // テキストボックスの値を取得して初期化 const inputText = document.getElementById("add-text").value; document.getElementById("add-text").value = ""; // divタグ作成 const div = document.createElement("div"); div.className = "list-row"; // liタグ作成 const li = document.createElement("li"); li.innerText = text; // 完了ボタン作成 const completeButton = document.createElement("button"); completeButton.innerText = "完了"; // 削除ボタン作成 const deleteButton = document.createElement("button"); deleteButton.innerText = "削除"; //divタグの子要素に各要素を追加 div.appendChild(li); div.appendChild(completeButton); div.appendChild(deleteButton); // 未完了リストに追加 document.getElementById("incomplete-list").appendChild(div); }; document .getElementById("add-button") .addEventListener("click", () => onClickAdd()); 解説 テキストボックスに入力した値を取得し、TodoのHTML構造が以下のようになるように各要素を作成して追加しています。 <div class="list-row"> <li>Todo</li> <button>完了</button> <button>削除</button> </div> Todo削除 import "./styles.css"; const onClickAdd = () => { // テキストボックスの値を取得して初期化 const inputText = document.getElementById("add-text").value; document.getElementById("add-text").value = ""; // divタグ作成 const div = document.createElement("div"); div.className = "list-row"; // liタグ作成 const li = document.createElement("li"); li.innerText = text; // 完了ボタン作成 const completeButton = document.createElement("button"); completeButton.innerText = "完了"; // 削除ボタン作成 const deleteButton = document.createElement("button"); deleteButton.innerText = "削除"; //--------------------追加--------------------------------------------------- deleteButton.addEventListener("click", () => { // 削除する部分の親要素を取得 const deleteTarget = div.parentNode; // 親要素から削除 document.getElementById("incomplete-list").removeChild(deleteTarget); }); //-------------------------------------------------------------------------- //divタグの子要素に各要素を追加 div.appendChild(li); div.appendChild(completeButton); div.appendChild(deleteButton); // 未完了リストに追加 document.getElementById("incomplete-list").appendChild(div); }; document .getElementById("add-button") .addEventListener("click", () => onClickAdd()); 解説 削除ボタンをクリックしたらparentNodeで削除する部分を取得し、removeChildで削除するfunctionを作りTodoを削除しています。 Todo完了(共通化) import "./styles.css"; const onClickAdd = () => { // テキストボックスの値を取得して初期化 const inputText = document.getElementById("add-text").value; document.getElementById("add-text").value = ""; // divタグ作成 const div = document.createElement("div"); div.className = "list-row"; // liタグ作成 const li = document.createElement("li"); li.innerText = text; // 完了ボタン作成 const completeButton = document.createElement("button"); completeButton.innerText = "完了"; //-------------------------追加------------------------------------------ completeButton.addEventListener("click", () => { //押された完了ボタンの親タグをリストから削除 deleteFromIncompleteList(completeButton.parentNode); }); //----------------------------------------------------------------------- // 削除ボタン作成 const deleteButton = document.createElement("button"); deleteButton.innerText = "削除"; deleteButton.addEventListener("click", () => { //-------------------------追加------------------------------------------ //押された削除ボタンの親タグをリストから削除 deleteFromIncompleteList(deleteButton.parentNode); //----------------------------------------------------------------------- }); //divタグの子要素に各要素を追加 div.appendChild(li); div.appendChild(completeButton); div.appendChild(deleteButton); // 未完了リストに追加 document.getElementById("incomplete-list").appendChild(div); //-------------------------追加------------------------------------------ // targetの親要素を削除 const deleteFromIncompleteList = (target) => { document.getElementById("incomplete-list").removeChild(target); }; //----------------------------------------------------------------------- }; document .getElementById("add-button") .addEventListener("click", () => onClickAdd()); 解説 削除ボタンと完了ボタンで同じ処理(Todoをリストから消す)は共通化しておきます。 Todo完了 import "./styles.css"; const onClickAdd = () => { // テキストボックスの値を取得して初期化 const inputText = document.getElementById("add-text").value; document.getElementById("add-text").value = ""; // divタグ作成 const div = document.createElement("div"); div.className = "list-row"; // liタグ作成 const li = document.createElement("li"); li.innerText = text; // 完了ボタン作成 const completeButton = document.createElement("button"); completeButton.innerText = "完了"; completeButton.addEventListener("click", () => { //押された完了ボタンの親タグをリストから削除 deleteFromIncompleteList(completeButton.parentNode); //-------------------------追加------------------------------------------ //完了リストに追加する const addTarget = completeButton.parentNode; // todo内容の取得 const text = addTarget.firstElementChild.innerText; // 完了リストに追加するために初期化 addTarget.textContent = null; // liタグ作成 const li = document.createElement("li"); li.innerText = text; // 戻すbutton作成 const backButton = document.createElement("button"); backButton.innerText = "戻す"; // 親要素に子要素追加 addTarget.appendChild(li); addTarget.appendChild(backButton); // 完了リストに追加 document.getElementById("complete-list").appendChild(addTarget); //--------------------------------------------------------------------- }); // 削除ボタン作成 const deleteButton = document.createElement("button"); deleteButton.innerText = "削除"; deleteButton.addEventListener("click", () => { //押された削除ボタンの親タグをリストから削除 deleteFromIncompleteList(deleteButton.parentNode); }); //divタグの子要素に各要素を追加 div.appendChild(li); div.appendChild(completeButton); div.appendChild(deleteButton); // 未完了リストに追加 document.getElementById("incomplete-list").appendChild(div); // targetの親要素を削除 const deleteFromIncompleteList = (target) => { document.getElementById("incomplete-list").removeChild(target); }; }; document .getElementById("add-button") .addEventListener("click", () => onClickAdd()); 解説 完了リストに以下のHTMLのように追加するので、Todo内容を取得後、初期化して新しくliタグと完了ボタンを作成し、完了リストに追加しています。 <div class="list-row"> <li>TODOです</li> <button>戻す</button> </div> Todoを戻す import "./styles.css"; const onClickAdd = () => { // テキストボックスの値を取得して初期化 const inputText = document.getElementById("add-text").value; document.getElementById("add-text").value = ""; createIncompleteList(inputText); //共通化 }; //-------------------------追加------------------------------------------ // targetの親要素を削除 const deleteFromIncompleteList = (target) => { document.getElementById("incomplete-list").removeChild(target); }; //---------------------------------------------------------------------- const createIncompleteList = (text) => { //共通化 // divタグ作成 const div = document.createElement("div"); div.className = "list-row"; // liタグ作成 const li = document.createElement("li"); li.innerText = text; // 完了ボタン作成 const completeButton = document.createElement("button"); completeButton.innerText = "完了"; completeButton.addEventListener("click", () => { // リストから削除 deleteFromIncompleteList(completeButton.parentNode); //完了リストに追加する const addTarget = completeButton.parentNode; // todo内容の取得 const text = addTarget.firstElementChild.innerText; // 完了リストに追加するために初期化 addTarget.textContent = null; // liタグ作成 const li = document.createElement("li"); li.innerText = text; // textに変更 // 戻すbutton作成 const backButton = document.createElement("button"); backButton.innerText = "戻す"; //-------------------------追加------------------------------------------ backButton.addEventListener("click", () => { // 完了リストから削除 const deleteTaget = backButton.parentNode; document.getElementById("complete-list").removeChild(deleteTaget); // テキスト取得 const text = backButton.parentNode.firstElementChild.innerText; createIncompleteList(text); }); //---------------------------------------------------------------------- // 親要素に子要素追加 addTarget.appendChild(li); addTarget.appendChild(backButton); // 完了リストに追加 document.getElementById("complete-list").appendChild(addTarget); }); // 削除ボタン作成 const deleteButton = document.createElement("button"); deleteButton.innerText = "削除"; deleteButton.addEventListener("click", () => { deleteFromIncompleteList(deleteButton.parentNode); }); // divタグの下にliタグを配置 div.appendChild(li); div.appendChild(completeButton); div.appendChild(deleteButton); // 未完了リストを配置 document.getElementById("incomplete-list").appendChild(div); }; document .getElementById("add-button") .addEventListener("click", () => onClickAdd()); 解説 フォームに値を入力し追加ボタンを押して未完了リストに使いした処理と同じなので共通化しています。 これで完成!! 参考
- 投稿日:2021-10-13T11:59:06+09:00
JavaScript概念
JavaScriptの概念的な用語を簡単にまとめてみました。 DOM DOMとは「Document Object Model」の略で、HTMLを解釈してツリー構造で表したもの。 JavaScriptはDOMを操作することでHTMLの見た目を変えている。 DOMはディベロッパーツールから見ることができる。 素のJavaScriptやjQueryではDOMを直接操作している。 仮想DOM JavaScriptのオブジェクトで仮想的に作られたDOMのこと。 DOMを直接操作するとレンダリングコストやコードが複雑化してしまうが仮想DOMはそれを解決してくれる。 仮想DOMはいきなりDOMを操作せずにJS上で仮想DOMを操作し、差分を出してからDOMに反映している。 npm/yarn 従来のパッケージマネージャーでは、1つのJSファイルに全ての処理を記述していたり、他のJSファイルを読み込んで使っていたが、これだとコードがカオスかしたり、読み込み順を意識しないとエラー、どこから読み込まれているか分からないとう問題点があった。 この問題を解決するためにnpm/yarnが誕生。 npm/yarnは依存関係勝手に解決してくれたり、import先が明示的で読み込み先が分かりやすく、世界中に公開されているパッケージをボタン一つで利用できる。 ECMAScript 通称えくますくりぷと JSの標準規格(JSの書き方のルール) ES6(ES2015)の大幅アップデートで機能追加が多くありJSの転換期と言われているらしい。 letやconstを用いた変数宣言 アローファンクション 分割代入 スプレッド構文 etc... などが様々な機能が追加された。 モジュールバンドラー 複数のJS(css/image)ファイルを1つにまとめるためのものでwebpackが代表的。 分けて開発していたファイルを本番環境では1つにまとめてくれる。 トランスパイラ 新しいJavaScriptの記法を古い記法に変換してくれるもの。BABELが代表的。 JSはブラウザ上で動くものだが、新しいJSの記法に対応していないブラウザもある。トランスパイラを使用すると、新しい記法で開発し、実行するときにトランスパイラが変換してくれるのでどのブラウザでも実行ができる。 SPA シングルページアプリケーションの略。HTMLは1つでJSで画面を書き換える技術。 従来のwebシステムはリクエストに応じた画面(HTML)を都度返却する。 SPAは最初は同じようにHTMLを返却するが、2回目以降は画面表示に必要なデータだけを返却し、JSでHTMLを書き換えることで画面が変わったように見える。 SPAのメリットは、 ページ遷移ごとのチラつきがなくなる。 表示速度のアップによりユーザー体験の向上。 コンポーネント分割による開発効率のアップ。 などがある。 参考
- 投稿日:2021-10-13T10:54:18+09:00
MathJaxを動的に処理した
初めに どうも、最近 Ruby on Rails 初めた人です。(記事と全く関係ない) 今回の記事は。 MathJaxというライブラリを使って数学のwebアプリを作っていたとき、動的に処理できなくて困ったので記事にまとめます。 MathJaxってなに? MathJax はMathML、LaTeX、ASCIIMathMLで記述された数式をウェブブラウザ上で表示するクロスブラウザのJavaScriptライブラリである。MathJaxはApache Licenseのもとでオープンソースソフトウェアとしてリリースされている。 MathJax - Wikipedia まぁ、要するに「カッコいい数式を表示できる機能」ってこと 例 二次方程式 [x=ax^2+bx+c] の解は \\ x = {-b \pm \sqrt{b^2-4ac} \over 2a} こんなかんじ どうやって使うの? <script type="text/javascript" id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"> </script> ↑こいつを <head> タグの中に入れるだけ! あとは、 <body> タグの中に数式を放り投げるだけ。 数式は、ぐぐったら出てくるから、自分で調べて。 自分がしたかったこと ボタンを押して数式を表示する 「スタートボタンを押して、事前に用意してあった数式を表示して、回答する」 という簡単な計算アプリを作っていた。 数式を表示するだけならすぐに実行できたが、.innerHTML もしくは .html を書き換えると、うまく行かなくなった。 結論 // head <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/5.0.4/math.min.js"></script> <script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script> <script type="text/javascript" src="script.js" charset="utf-8"></script> // body <div id="math">\\begin\{align\}半径 = \\frac{直径}{2}\\end\{align\}</div> <button onClick="changeMath()"></button> // script.js function changeMath() { $("#math").html("\\begin\{align\}"); $("#math").append("y=\\sin(\\theta+\\frac{\\pi}{2})+1"); $("#math").append("\\end\{align\}"); MathJax.Hub.Queue(["Typeset",MathJax.Hub,"quMain"]); } 解説 表示されたボタンを押すと changeMath() が発火する。 そうすると、表示されていた数式が上の数式から下の数式に置き換わる。 半径 = { 直径 \over 2 } y=\sin(\theta+\frac{\pi}{2})+1 以上
- 投稿日:2021-10-13T08:59:01+09:00
【JavaScript】変数と参照の振り返り⑥ 参照とコピーについて
はじめに Udemyの【JS】ガチで学びたい人のためのJavaScriptメカニズムの講座の振り返りです。 前回の記事 目的 変数についての理解を深める 本題 1.プリミティブ型とオブジェクト 用語の説明 データ型:文字列、数値などの異なる値の型 → 8種類 このデータ型は2種類に分けることができる。 プリミティブ型 変数には値が格納される。 一度作成するとその値を変更することはできない。 オブジェクト プリミティブ型以外の型を指す。 変数には参照が保持される。 値を変更することができる。 名前(プロパティ)付きの参照を管理する入れ物 2.参照とコピー プリミティブ型とオブジェクトで値をコピーした場合には挙動が変わる。 例 プリミティブ値のコピー 参照先の値がコピーされる。 // プリミティブ型 // 変数aはhelloという値への参照を保持している let a = "Hello"; // 変数bをaに代入 // ここで変数bにもhelloという値への参照をコピーした let b = a; // 変数aの参照先である値自体が変更されるわけではない // bに"bye"が格納されるだけでaに代入されたわけではない b = "bye"; console.log(a,b); // それぞれの値が独立しているのでbを変更したからaも変わるというわけではない。 オブジェクトのコピー オブジェクトへの参照がコピーされる。 let c = { prop: "hello" } let d = c; // 値を"bye"に変更すると d.prop = "bye"; // どちらも"bye"に変更されてしまう console.log(c,d); // これはcもdも同じオブジェクトへの参照を保持しているため propを変更するのではなく、dに新しい値を入れるとどうなるか。 let c = { prop: "hello" } let d = c; // 変数dにオブジェクトを定義 d = {}; // このようにすると互いの変数に影響がない // それぞれの{...}への参照をコピーしているため console.log(c,d); 今日はここまで! 参考にさせて頂いた記事 【JS】ガチで学びたい人のためのJavaScriptメカニズム
- 投稿日:2021-10-13T07:14:40+09:00
これからReact始めたい人のための今日だけでできるTODO#21 useContext② コンポーネントの分割
分割したコンポーネントでuseContextを使ってみる 以下のような構成でコンポーネントを作成しAppをProviderでラップしてThirdChildにContextを使って値を渡したいと思います。 App FirstChild SecondChild ThirdChild コンポーネントの作成 作成するファイルは4つです(App.jsはすでにある想定です) - Provider.js - FirstChild.js - SecondChild.js - ThirdChild.js Provider.js Provider.jsのコードは以下の通りになります。 Provider.js import React, { createContext } from 'react'; // 他のファイルでContextを参照できるようにexport export const Context = createContext(); const text = 'Providerから受け取ったテキスト'; // Contextオブジェクトを参照できるように`Provider`でラップ export const Provider = ({ children }) => { return <Context.Provider value={text}>{children}</Context.Provider>; } App.js App.js import { Provider } from './Provider'; import { FirstChild } from './FirstChild'; export default function App() { return ( <> // ProviderでタップしているのでFirstChild配下でContextオブジェクトの値が参照できる <Provider> <FirstChild /> </Provider> </> ); }; FirstChild.js FirstChild.js import { SecondChild } from './SecondChild'; export const FirstChild = () => { return ( <> <p>FirstChild</p> <SecondChild /> </> ); }; SecondChild.js SecondChild.js import { ThirdChild } from './ThirdChild'; export const SecondChild = () => { return ( <> <p>SecondChild</p> <ThirdChild /> </> ); }; ThirdChild.js ThirdChild.js import React, { useContext } from 'react'; import { Context } from './Provider'; export const ThirdChild = () => { const textData = useContext(Context); return ( <> <p>ThirdChild:{ textData }</p> </> ); }; すると画像のようになります。 Provider.jsでvalueに設定した値がThirdChildで受け取り、表示させることができました。 useStateを利用したデータを渡したい 変化するデータを受け渡したい場合も当然出てくると思います。 そんな時は下記のように変更すれば良いと思います。 Provider.js // useStateを追加する import React, { useState, createContext } fron 'react'; // Providerの中でuseStateを定義する export const Provider = ({ children }) => { //追加 const [state, setState] = useState(false); //valueの渡し方を配列に変更 return <Context.Provider value={[state, setState]}>{children}</Context.Provider>; } ThirdChild.js // stateとsetStateを定義してContextオブジェクトから受け取った値を代入する const [state, setState] = useContext(Context); // click時の処理を追加 const handleClick = () => { setState(!state); }; <> <p>ThirdChild:現在のステータス{state ? 'true':'false'}</p> <button onClick={handleClick}>state変更ボタン</button> </> これでクリックしたらステータスによってtrueとfalseのテキスト表示が変わるようになったと思います。
- 投稿日:2021-10-13T06:44:25+09:00
LINE Message API を活用して、ポケモン対戦がやりやすくなるツールを作成
前回初めて書いた記事は真面目に考察したので、今回は自分の趣味の一部を交えて、少し読みやすいテーマ・内容としてみました。 ポケモン対戦は覚えることが多い ポケモン対戦をやったことある方は、この章については読み飛ばしていただいて問題ありません。 なぜそもそも、ポケモン対戦を題材としたのかということについてです。自分の趣味ということもありますが、最大の理由として、ポケモン対戦は覚えることが非常に多く、初心者の参入障壁が高いという問題点があると考えているためです。 現在、ポケモン対戦は毎年世界大会が実施され、国内でも新作が発売されると、10万人程度が参加するコンテンツとなりました。youtubeなどの動画サイトでも対戦の模様は動画として上がっており、もし興味が湧いたら検索してご覧いただきたいのですが、まあ覚えることが多い。 種族値・個体値・努力値(公式名称では無いです。) ポケモンの特性 技の効果 対戦でよく使われる育成方法、いわゆる型 などなど 初心者が負けて覚えるのは当然ですが、それでも少しでも勝つ喜びを味わってもらう補助ができないかと考え、ツールの作成に着手しました。 対戦中の負担を軽減する 対戦始めたばかりの方が、少しでも対戦しやすくなるツール 自分が対戦中にど忘れした際、簡単に確認できるツール この2点を意識して、表示する項目を選定しました。 今回は対戦の勝敗に直結すると考えている 特性 素早さ 慣れていてもど忘れしてしまう 威力が相手の体重に依存する技の威力 この3つを、まず簡単に調べることができるツールに決定。 さらに、対戦中は常に90秒という選択時間に追われることを考え、パッと入力してパッと答えが返ってくる上に扱いが簡単な、linebotをインターフェイスとして活用することとしました。 なぜ、素早さが対戦の勝敗に直結するかは、非常に長くなるので本記事では割愛します。 環境・利用API node v16.10.0 Visual Studio Code 1.60.2 axios 0.22.0 ngrok 2.3.40 PokeAPI https://pokeapi.co/ ポケモンのAPIとしては他にもありましたが、データの網羅性が高く、今後の機能拡張がしやすい点を考えて、PokeAPIを選択しました。 サンプルコード LINE Messaging APIとPokeAPI それぞれのコードを記載します。 LINE Messaging API / Axiosのコード 'use strict'; // ######################################## // 初期設定など // ######################################## // パッケージを使用します const express = require('express'); const line = require('@line/bot-sdk'); const axios = require('axios'); // ローカル(自分のPC)でサーバーを公開するときのポート番号です const PORT = process.env.PORT || 3000; // Messaging APIで利用するクレデンシャル(秘匿情報)です。 const config = { channelSecret: '作成したBotのチャネルシークレット', channelAccessToken: '作成したBotのチャネルアクセストークン' }; // ########## ▼▼▼ サンプル関数 ▼▼▼ ########## (この行をサンプル関数丸ごと全部と置き換えてね) // ########## ▲▲▲ サンプル関数 ▲▲▲ ########## // ######################################## // LINEサーバーからのWebhookデータを処理する部分 // ######################################## // LINE SDKを初期化します const client = new line.Client(config); // LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます async function handleEvent(event) { // 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } // サンプル関数を実行します return sampleFunction(event); } // ######################################## // Expressによるサーバー部分 // ######################################## // expressを初期化します const app = express(); // HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします app.post('/webhook', line.middleware(config), (req, res) => { // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行 if (req.body.events.length === 0) { res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します(なくてもよい) console.log('検証イベントを受信しました!'); // ターミナルに表示します return; // これより下は実行されません } else { // 通常のメッセージなど … Webhookの中身を確認用にターミナルに表示します console.log('受信しました:', req.body.events); } // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、 // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result)); }); // 最初に決めたポート番号でサーバーをPC内だけに公開します // (環境によってはローカルネットワーク内にも公開されます) app.listen(PORT); console.log(`ポート${PORT}番でExpressサーバーを実行中です…`); PokeAPIによるデータ取得 const getPoke = async (userId, tag) => { let result = ''; try { // axiosでQiitaのAPIを叩きます const res = await axios.get('https://pokeapi.co/api/v2/pokemon/' + encodeURIComponent(tag)); const item = res.data; const speed = item.stats[5].base_stat; const weight = item.weight; let powerWeight = ''; if(weight < 90){ powerWeight = '20'; } else if(weight < 249){ powerWeight = '40'; } else if(weight < 499){ powerWeight = '60'; } else if(weight < 999){ powerWeight = '80'; } else if(weight < 1999){ powerWeight = '100'; } else{powerWeight ='120' } try{ const abi2 = item.abilities[2].ability.name;//特性が3つある const abi0 = item.abilities[0].ability.name; const abi1 = item.abilities[1].ability.name; result = `特性1 ${abi0} 特性2 ${abi1} 特性3 ${abi2} 素早さ種族値 ${speed} くさむすび、けたぐりの威力${powerWeight}`; console.log(`「${tag}」`); } catch(error) { try{const abi1 = item.abilities[1].ability.name;//特性が2つある const abi0 = item.abilities[0].ability.name; result = `特性1 ${abi0} 特性2 ${abi1} 素早さ種族値 ${speed} くさむすび、けたぐりの威力${powerWeight}`; console.log(`「${tag}」`); } catch(error) {const abi0 = item.abilities[0].ability.name; result = `特性1 ${abi0} 素早さ種族値 ${speed} くさむすび、けたぐりの威力${powerWeight}`; console.log(`「${tag}」`); } } // 正常に取得できればここで終了 } catch (error) { // HTTPステータスコードが404ならタグが見つからない、それ以外は別のHTTPエラーです const { status, statusText } = error.response; if (status == 404) { result = 'ポケモンの名前を正確に入力してください。英語で。'; } else { result = `エラー: ${status} ${statusText}`; } } // リプライではなく「プッシュ」を送ります // Botからユーザーへ一方的に通知を送ることができる機能です await client.pushMessage(userId, { type: 'text', text: result, }); } const sampleFunction = async (event) => { // ユーザーIDとメッセージ文字列を関数に渡します // メッセージ文字列でQiitaタグを検索し、結果をQiitaから受け取ったらユーザーIDに対して「プッシュ」送信します // 検索には少し時間がかかるので、これの結果は後でユーザーに送られます getPoke(event.source.userId, event.message.text); // こちらが先に返事を返します // ユーザーからのメッセージに対する「リプライ」です // リプライは、受信したメッセージ1つにつき1回しか使えません return client.replyMessage(event.replyToken, { type: 'text', text: '検索しています……' }); }; 特性の数が変わると、ポケモンによってJSONの形式が異なるため、単純な繰り返し処理でのデータ取得ではエラーが発生するため、苦戦しました。 今回は例外処理を用いましたが、コードが非常に汚いので改善が必要かと思います。 動作 ポケモンの名前を英語で入力すると、特性、素早さ、体重依存の技を受けるときの威力が返ってきます。 必要なデータがサクサクと取れるので、対戦中確認したいときには一々ネットで調べる必要もなく便利です。 今後の追加機能 日本語化機能 ダメージ計算機能 体重差に応じた技威力の表示 まずは、日本語化でしょうか。私のようにニックネームを英語にしたいから英語でプレイするという方ばかりではないと思いますので。 他には、入力した内容を判別して、〇〇が✖️✖️で△△を攻撃した時のダメージは□□といった具合で、ダメージ計算ツールも実装できればより使いやすいかと考えています。ダメージ計算ツール自体は巷に溢れているため、ひとまずの実装優先度は低いと考えて一旦見送り。 また、攻撃する側とされる側の体重差に応じて技威力が変わる技については、あまり簡単に計算してくれるツールが見当たらないので、できれば実装したいのですが、これも該当する技の利用頻度が低いため、ひとまず見送っています。
- 投稿日:2021-10-13T02:20:23+09:00
microCMSのコンテンツをmarkdownファイルで取得する方法
microCMSのAPIでコンテンツを取得して、それぞれエンドポイントごとに個別のファイルを作成する必要ことがあった。取得したファイルを編集するため、HTMLではなく、markdownファイルで取得する必要があった。その時の記録を残しておく。 なお、npmは使わず、CDNのみを利用するため、Nodeやwebpackがわからない人もこの方法で取得できる。 microCMS まずは、microCMSのAPI取得である。公式ドキュメントにもあるように、serviceDomain と apiKey を利用して取得できる。このapiKeyには2種類でもある。X-API-KEYとX-GLOBAL-DRAFT-KEYである。 GETによる取得は前者のX-API-KEYを利用し、下書きコンテンツも含めて取得する場合はX-GLOBAL-DRAFT-KEYを利用する。 今回はX-API-KEYを利用する。 まずは、htmlのCDNでmicroCMSを読み込み、jsファイルにimportする。jsファイルではclientでドメインとAPIキーと共に初期化する。 index.html <script src="https://unpkg.com/microcms-js-sdk@latest/dist/umd/microcms-js-sdk.js"></script> main.js const { createClient } = microcms; const client = createClient({ serviceDomain: "ドメイン", apiKey: "ご自身のX-API-KEY" }); 次に、GET /api/v1/{endpoint} の形式で取得する。client.getではendpointに配列からそれぞれの値を取得する。取得したオブジェクトは定数materialContentsに保存する。awaitで記述するのはこの関数を次に、関数でasyncを定義しているからだ。 main.js const endpointArray = ['category1','category2','category3']; const category = endpointArray[1]; const data = await client.get({ endpoint: category, queries: { offset, limit } }) const materialContents = data.contents; materialContents に保存するオブジェクト内に必要なデータはタイトル、記述したコンテンツブロックである。以下のようにオブジェクトは取得できる。 turndownService 取得したHTMLをmarkdown化するのが Turndown というモジュールである。 これもCDNで利用できる。下にあるCDNはこのTurndownのプラグインでこれがあることでテーブルタグもhtmlからmarkdown表記に変更できる index.html <script src="https://unpkg.com/turndown/dist/turndown.js"></script> <script src="https://unpkg.com/turndown-plugin-gfm/dist/turndown-plugin-gfm.js"></script> このturndownを利用して、以下のようなソースコードで取得して、その内容をマークダウン化した。 取得したオブジェクトからタイトルを取得して変数に代入する。その後、contentの配列は三項演算子でrichEditorかhtmlかで取得する内容を変更している。それらをDOMでタイトルはpタグにコンテンツブロックの内容は一つの記事ごとにまとめてdivタグに代入している。そして、タイトルに合わせた記事内容が表記されるようにappendChildでそれぞれの内容の結合を行った後、それをturndownでmarkdownにしている。 const { createClient } = microcms; const gfm = turndownPluginGfm.gfm; let turndownService = new TurndownService({ headingStyle: 'atx' , codeBlockStyle: 'fenced', preformattedCode: 'true'}); turndownService.use(gfm); // いくつかのoptionがある。headingStyle: 'atx'h2が##で表示される const client = createClient({ serviceDomain: env.Domain, apiKey: env.apiKey }); const endpointArray = ['category1','category2','category3','category4','category5']; const getAllContents = async (limit=100, offset=0) => { const category = endpointArray[0];//好きなendpointのカテゴリを指定する const data = await client.get({ endpoint: category, queries: { offset, limit } }) const materialContents = data.contents; const mainContent = document.getElementById("mainContent"); materialContents.forEach(element => { const p = document.createElement("p"); const div = document.createElement("div"); let title = `# ${element.title}`; let chapterContets = ""; for (let i = 0; i < element.content.length ; i++) { if(element.content[i]) { let codeBlock = (element.content[i].richEditor) ? element.content[i].richEditor : element.content[i].html; chapterContets += codeBlock; //各コンテンツごとに変数に格納。reduceで書いたもよかった気がする。 } div.innerHTML = chapterContets; } p.innerHTML = title; p.appendChild(div); mainContent.appendChild(p); //mainContentに取得したデータが全て格納されている。 }); let mainMd = turndownService.turndown(mainContent); blob 最後に、blobを用いて、htmlからmarkdown化させたmicroCMSの内容をファイルとしてダウンロードする。blobはインスタンス化する際に第1引数に変換する対象、第2引数にその形式を当てはめる。 本来はmd拡張子にしたかったが、確認できなかったため、txt取得する。もちろんファイルの中身はhtmlをそのままmarkdown化させているので大きな問題はない。 また、目的としてファイルダウンロードができれば、問題ないのでブラウザでhtmlを開いたら、目的のエンドポイントごとにmarkdown化されたファイルを取得できるようにした。 それを踏まえたのが下記のようなコードである。microCMSから取得したデータが格納されたpタグを生成した mainContent をまず、marakdown化して、その内容をblobで変換している。 main.js const { createClient } = microcms; const gfm = turndownPluginGfm.gfm; let turndownService = new TurndownService({ headingStyle: 'atx' , codeBlockStyle: 'fenced', preformattedCode: 'true'}); turndownService.use(gfm); // headingStyle: 'atx'h2が##で表示される const client = createClient({ serviceDomain: env.Domain, apiKey: env.apiKey }); const endpointArray = ['category1','category2','category3','category4','category5']; const getAllContents = async (limit=100, offset=0) => { const category = endpointArray[0]; const data = await client.get({ endpoint: category, queries: { offset, limit } }) const materialContents = data.contents; const mainContent = document.getElementById("mainContent"); materialContents.forEach(element => { const p = document.createElement("p"); const div = document.createElement("div"); let title = `# ${element.title}`; let chapterContets = ""; for (let i = 0; i < element.content.length ; i++) { if(element.content[i]) { let codeBlock = (element.content[i].richEditor) ? element.content[i].richEditor : element.content[i].html; chapterContets += codeBlock; } div.innerHTML = chapterContets; } p.innerHTML = title; p.appendChild(div); mainContent.appendChild(p); //mainContentに取得したデータが全て格納されている。 }); let mainMd = turndownService.turndown(mainContent); const blob = new Blob([mainMd],{type: 'text/plain' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `${category}.txt`; link.click(); if (data.offset + data.limit < data.totalCount) { const contents = await getAllContents(data.limit, data.offset + data.limit) return [ ...data.contents, ...contents ] } return data.contents } getAllContents(); まとめ このように、microCMSのAPIを元になかなか行わないであろうHTMlからmarkdownファイルへの変換を行うことができた。また、CDNなのでnpmでNode環境を利用しないことも簡潔で良いだろう。ただ、ネストを深めれば、カテゴリ用の配列から手動で行わずにファイルを取得する方法もあったり、無駄に冗長になった処理もあるのでコードのシェイプアップする必要はある。 こちらがgithubのソースコードである。
- 投稿日:2021-10-13T02:18:30+09:00
Nuxt3のpublic beta版がついに公開されたので、さっそく導入してみた
はじめに Vue3に対応したフレームワークであるNuxt3のPublic beta版が 2021/10/13についに公開されました。 まずは公式ドキュメントの内容を紹介しつつ、 プロジェクトの作成を試してみたいと思います。 ※詳細は個人ブログこちらから御覧ください。
- 投稿日:2021-10-13T01:42:54+09:00
デブ活必需品!? お菓子の虜LINE bot
とにかく甘いお菓子が好き? おしゃれなスイーツも、手作りケーキもおいしいけれど、私は断固「お菓子」推し!! なんと数百円で楽園へひとっとび…わかる人にはわかりますよね・・・? https://twitter.com/okinakamasayos1/status/1447864486598103041 https://twitter.com/okinakamasayos1/status/1423594038947500036 私の神サイト「お菓子の虜」? メーカーが書いた記事じゃなく、実際に食べ歩いたレビューがなんと2,600種類以上! お菓子好きにはたまらない「新商品」「期間限定商品」を重点的に紹介してくれます。 短くも美しく、想像力を掻き立てるレビューは必見ですよ~例えばこちら 「マロン」「梨」「ココア」など永遠にキーワード検索していられます。。。 APIで「お菓子の虜LINE bot」を作る? そんな神サイトにWebAPIを発見!覚えたてのLINEbotの技術を組み合わせて、 キーワードを打ち込むと最新のお菓子情報が返信されるLINEbotを作りました! チョコ?ピスタチオ? ふっと浮かんだキーワードから、 瞬時にあなたを最新お菓子レビューへ誘います! 完成品・コードははこちら サンプルコード (クリックで表示) 'use strict'; // ######################################## // 初期設定など // ######################################## // パッケージを使用します const express = require('express'); const line = require('@line/bot-sdk'); const axios = require('axios'); // ローカル(自分のPC)でサーバーを公開するときのポート番号です const PORT = process.env.PORT || 3000; // Messaging APIで利用するクレデンシャル(秘匿情報)です。 const config = { channelSecret: 'チャンネルシークレット', channelAccessToken: 'アクセストークン' }; const getQiitaTag = async (userId, tag) => { let result = ''; try { // axiosでお菓子のAPIを叩きます const res = await axios.get('https://sysbird.jp/webapi/?apikey=guest&keyword=' + encodeURIComponent(tag)+'&max=1&format=json'); const item = res.data; result = '最新の「' + tag + '」関連お菓子は、' + item.item.regist + '紹介、\r\n「' + item.item.name + '」\r\n!詳しくはこちら(^^♪ \r\n' + item.item.url + '!'; // ターミナルにも検索結果を出しておきます console.log(`「${tag}」の検索結果:${item.item.name}`); // 正常に取得できればここで終了 } catch (error) { result = '「' + tag + '」のお菓子はまだないみたい。\r\n開発者のつもりで想像してみて♪\r\n私の一押しはこれ!https://sysbird.jp/toriko/2021/10/04/%e8%b4%85%e6%b2%a2%e3%83%ab%e3%83%9e%e3%83%b3%e3%83%89/'; } // リプライではなく「プッシュ」を送ります // Botからユーザーへ一方的に通知を送ることができる機能です await client.pushMessage(userId, { type: 'text', text: result, }); } const sampleFunction = async (event) => { getQiitaTag(event.source.userId, event.message.text); // こちらが先に返事を返します // ユーザーからのメッセージに対する「リプライ」です // リプライは、受信したメッセージ1つにつき1回しか使えません return client.replyMessage(event.replyToken, { type: 'text', text: 'あま~く詮索中……????' }); }; // ######################################## // LINEサーバーからのWebhookデータを処理する部分 // ######################################## // LINE SDKを初期化します const client = new line.Client(config); // LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます async function handleEvent(event) { // 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } // サンプル関数を実行します return sampleFunction(event); } // ######################################## // Expressによるサーバー部分 // ######################################## // expressを初期化します const app = express(); // HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします app.post('/webhook', line.middleware(config), (req, res) => { // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行 if (req.body.events.length === 0) { res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します(なくてもよい) console.log('検証イベントを受信しました!'); // ターミナルに表示します return; // これより下は実行されません } else { // 通常のメッセージなど … Webhookの中身を確認用にターミナルに表示します console.log('受信しました:', req.body.events); } // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、 // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result)); }); // 最初に決めたポート番号でサーバーをPC内だけに公開します // (環境によってはローカルネットワーク内にも公開されます) app.listen(PORT); console.log(`ポート${PORT}番でExpressサーバーを実行中です…`); 甘いだけじゃない苦労話? ①日本語tag検索どうすればいいのか問題 + encodeURIComponent(tag)+を入力することで解決 ②JSON形式ではなかった問題 &format=jsonをAPIの最後に入力することで表記がJSONに ③画像が表示できない問題(未解決) JSONではjpegでデータを取得できているが、LINEで表示させられず、今回は断念。 ④長いURLではなくサムネイルだけ表示させたい(未解決) レビューサイトの長ったらしいURLではなく、サムネイル画像だけを表示させ クリックするとレビューサイトへ飛べるようにしたかったが、辿り着けなかった。 ⑤検索結果が「0」だのときのリプライ問題 try...catch文で「0」をエラーとして検知し、別のメッセージを表示させた。 当初catch文の中にif文で組んでいたがうまくいかず、 結果シンプルに下記のコードで実行できた。 { status: 'OK', count: '0' }というJSONデータが帰ってきており、 これをどのような理由で「error」と検知しているのか、今の自分にはわからなかった。 } catch (error) { result = '「' + tag + '」のお菓子はまだないみたい。 努力は夢中に勝てない? 実はLINE botを作るのは今回が2回目。自分の大好きなテーマで制作したので 難しいながらも夢中で取り組め、理解も進んだと思う。今後は業務にも結び付けたいが、 いつも自分の「好き」や「やってみたい」に真摯に向き合っていたいな~と お菓子を食べながら振り返る深夜1時30分でした?
- 投稿日:2021-10-13T00:26:20+09:00
JavaScriptのEvent Propagationとは?
初めに 今回はイベントリスナーを設定した時のイベントの伝播方法について、詳しく見ていきます。突然ですが、下記のようなHTMLがあったとします。 <html> <body> <div onclick="console.log('clicked!')"> <button style="width: 100px; height: 50px">button</button> </div> </html> </body> onclickイベントを設定しているのは親のdiv要素ですが、ボタンをクリックしてもイベントが発火して「clicked!」が表示されました。考えてみれば不思議ですよね。なぜ実際のクリックがボタンに対してなされてもdiv上のハンドラが実行されるのでしょうか。それは、イベントプロパゲーションという仕組みが背後で作用しているためなのです。 イベントプロパゲーション イベントプロパゲーションとは、イベントがどのようにDOMツリーを伝搬してターゲットに到達し、戻ってくるかを定義するメカニズムです。 イベントプロパゲーションのコンセプトを理解するためには、まずDOMが階層構造を有していることを理解する必要があります。例えば、上記のHTMLの例を図にして詳しく見てみると以下のようになります。 DOMツリーの最上位にWindowオブジェクトが配置されています。その下にDocumentオブジェクトが配置され、HTMLの階層と同様の階層構造がDocumentの下に配置されます。では階層構造の一番下に位置するボタンエレメントでクリックイベントが発生した際のイベント伝播の流れを見ていきましょう。 イベントプロパゲーションは「キャプチャリング」、「ターゲット」、「バブリング」の3つのフェーズに大きく分けられます。 キャプチャリングフェーズ && ターゲットフェーズ HTMLページの中のbuttonがクリックされると、まずDOMツリーの一番最上位であるWindowオブジェクトでClickイベントが発生します。そしてDocumentオブジェクト、htmlエレメント、bodyエレメント、divエレメントとDomツリーを下に向かって実際にイベントが発生したターゲットエレメントを特定します。この過程をキャプチャリングフェーズと呼び、イベントがターゲット要素に到達した段階をターゲットフェーズと呼びます。 バブリングフェーズ バブリングフェーズでは、キャプチャリングフェーズと正反対のことが起きます。ターゲットエレメントからWindowオブジェクトまで、イベントが水底で生まれたバブル(泡)のように上がっていきます。要素内でイベントが起きると、最初にその要素のハンドラが実行され、次にその親のハンドラが実行され、さらにその親...というようにハンドラが駆け上がっていきます。イベントの発火は通常バブリングフェーズで実行されます。 本稿の最初に見た例に戻ると、button要素で検知したイベントがバグリングフェーズでdiv要素を経由するため、div要素でもイベントが検知され、div要素のハンドラが実行されたと考えることができます。 実験 では、実際にイベントプロパゲーションの流れを確認するために簡単な実験をしてみます。 まずは、ハンドラの実行順番についてのテストです。 <html> <body> <div> <button style="width: 100px; height: 50px">button</button> </div> </body> </html> <script> document .querySelector("html") .addEventListener("click", () => console.log("html clicked!")); document .querySelector("body") .addEventListener("click", () => console.log("body clicked!")); document .querySelector("div") .addEventListener("click", () => console.log("div clicked!")); document .querySelector("button") .addEventListener("click", () => console.log("button clicked!")); </script> html, body, div, button全てにイベントリスナーを設定しました。上記のHTMLのbutton要素をクリックした場合、ハンドラはどの順番で起きるでしょうか? 正解は、button要素からhtml要素まで、駆け上がっていくようにハンドラが実行されていきます。これはイベントのハンドラは通常バブリングフェーズで実行されるためです。 ...では、キャプチャリングフェーズで実行することはできないのか、という疑問も当然生じてきますよね。はい、できますとも。addEventListnerの第三引数をtrueに指定することで可能です。 <html> <body> <div> <button style="width: 100px; height: 50px">button</button> </div> </body> </html> <script> document .querySelector("html") .addEventListener("click", () => console.log("html clicked!"), true); document .querySelector("body") .addEventListener("click", () => console.log("body clicked!"), true); document .querySelector("div") .addEventListener("click", () => console.log("div clicked!"), true); document .querySelector("button") .addEventListener("click", () => console.log("button clicked!"), true); </script> 順番が逆になりましたね。一番親のhtml要素でハンドラが実行され、徐々に子要素へとイベントが伝播していっているのがわかります。 なお、今回はテストのためわざわざ順序を逆にしましたが、実際の開発でキャプチャリングフェーズでハンドラを実行させることはほとんどないと思われます。因みにaddEventListnerの第三引数はdefaultでfalseとなっているため、何も指定しなければイベントのハンドラはバブリングフェーズで実行されます。 ユースケース ではイベントプロパゲーションの仕組みを理解しておくとどんな時に役に立つのでしょうか。色々ありますが、例えばEvent Delegation(イベント移譲)と呼ばれるイベントハンドリングのパターンを実装する際なんかに役に立ちます。 イベント移譲とは、「似たようなイベントリスナーを多くの要素に対して設定する際、一つ一つにハンドラを割り当てる代わりに共通の祖先に一つハンドラを置く」という実装方法のパターンを指します。 具体的な例を見てみましょう。あるサイトのトップページのnavバーにボタンが三つあったと仮定します。そのボタンにはそれぞれページ内の要素が紐づけられており、クリックされるとその要素位置までスクロールされます。もしイベント委譲を使わずに書こうとするとボタンのエレメントをquerySelectorAllで全て取得してforEachでループ処理をするようになります。 <nav class="nav__items"> <ul> <li class="nav__item"> <a href="#section--1">セクション1</a> </li> <li class="nav__item"> <a href="#section--2">セクション2</a> </li> <li class="nav__item"> <a href="#section--3">セクション3</a> </li> </ul> </nav> <script> // .nav__itemを持つエレメントを全て取得してforEachでイテレート document.querySelectorAll('.nav__item').forEach(function (el) { el.addEventListener('click', function (e) { const id = this.getAttribute('href'); document.querySelector(id).scrollIntoView({ behavior: 'smooth' }); }); }); </script> しかし、このやり方では各ボタンを全てquerySelectorAllで取得して、全てに対してevent listenerをつけることになります。今回のようにボタンが3つだけの場合ではほとんど問題はないのですが、もしボタンが百個、千個を超えるようでしたらどうでしょうか。パフォーマンスに影響が出そうですよね。 そこで役に立つのがイベント委譲(Event delegation)の考え方です。共通の親要素に対してeventListenerをたった一つ付けることによって、同じことを実現します。 例を見ていきましょう。 <script> document.querySelector('.nav__items').addEventListener('click', function (e) { // クリックされたターゲットまでスクロール const id = e.target.getAttribute('href'); const target = document.querySelector(id); target.scrollIntoView({ behavior: 'smooth', }); }); </script> 上記のようにnav要素の親要素である にイベントリスナーを設定し、クリックされたターゲットをe.targetで取得します。こうすることによって、子要素がどれほど増えようとイベントリスナーの数を少なく保つことができ、パフォーマンスの改善にも役立つことができます。