20220214のJavaScriptに関する記事は15件です。

【実用編】【DB】discord.js v13 ユーザーごとにプロフィールを作成する

前書き 私がメインで開発している「ヒトリン」というボットにて、最近ユーザーごとにプロフィールを作成する機能を追加したため紹介します。 どのようなものか MongoDBを使用し、ユーザーごとに様々な情報を格納したデータを作成し、プロフィールを作成する。 今回は、MongoDBの使用方法からプロフィールを作成しコマンドで表示させるところまでのフルフルバージョンを紹介する予定でござんす。 MongoDB 上記のリンクからMongoDBのサイトへリンクしサインインまたは新規登録を完了してください。 ほとんど英語ですが頑張ってどんどん次に進みましょう。 一番右の「FREE」となっているものを選択し、「CREATE」をクリックしてください。 ここら辺は任意で選択してください。(なんもいじんなくても良いかもです) 右下にある「Create Cluster」をクリック ここら辺も適当でいいんですが、最後の「Where would you like to connect from?」は注意。(訳:あんちゃんはどっから接続するんや?) 特に理由がなければには右の「Add My Current IP Address」をクリックしてください。(意味:現在のIPアドレスを追加) 注意 IPアドレスを変更した場合、ここの設定から使用中のIPアドレスを追加してください。 完了したら「Finish and Close」をクリックしてください。 このような画面になれば設定は完了です。 npm MongoDBをNode.jsで使用するために「mongoose」をインストールします。 console:cmd npm install mongoose コーディング いよいよコーディングをしていきます。コーディングをしたくてしたくてたまらなく ☆禁断症状☆ が出ていた方もいらっしゃることかと存じます。ご安心ください。 ボットの核となるファイル「index.js」等に記述をしていきます。 index.js const mongoose = require('mongoose'); ~ ~ ~ mongoose //mongooseについて .connect(<接続するために必要>, { useNewUrlParser: true, //任意 }) .then(() => { console.log('データベースに接続したんだゾ'); }) .catch((error) => { console.log(error); //エラー出力 }); <接続するために必要> のところに入れるものを説明します。 先ほどのこの画面に来ます。 「Connect」をクリックし、「Connect your application」をクリック。 「Node.js」で、バージョンを任意のものに選択したら以下のようなよくわからない文字列が出てきます。 今回の例 mongodb+srv://test:<password>@cluster0.3afev.mongodb.net/<myFirstDatabase>?retryWrites=true&w=majority <password> には先ほど設定した「ユーザー」と「パスワード」の「パスワード」を入力 <myFirstDatabase> には任意のデータベースの名前を入力 そしたらその文字列を <接続するために必要> のところに入れます。 index.js mongoose //mongooseについて .connect(mongodb+srv://test:<password>@cluster0.3afev.mongodb.net/<myFirstDatabase>?retryWrites=true&w=majority, { useNewUrlParser: true, //任意 }) .then(() => { console.log('データベースに接続したんだゾ'); }) .catch((error) => { console.log(error); //エラー出力 }); そしたら次に任意のフォルダを作成し、そのフォルダの中に任意のJSファイルを作成します。今回は「models」というフォルダに「profileSchema.js」というJSファイルを作成します。 profileSchmea.js にはデータベースにどのようにどんな種類のデータを保存するのか、というものを書いていきます。 今回はユーザーのID、ユーザーネーム、アバターを保存していきます。 profileSchema.js const mongoose = require('mongoose'); //mongoDBを使用するためのおまじない const profileSchema = new mongoose.Schema({ _id: { type: String }, //ユーザーID name: { type: String }, //ユーザーネーム avatar: { type: String }, //アバター }); const model = mongoose.model('Profiles', profileSchema); module.exports = model; type: String 等の説明 type: の後には型が入ります。まぁプログラミング勉強してる方ならほぼ誰でもわかるかと思いますが一応紹介しておきます。 String: 文字列("あいうえお" "僕は天才、人生満喫中" 文字同士の計算はできない 例:"2" + "3" = "23" ) Number: 数字(1とか2とか。数字同士は計算できる 例:1+9=137193821738 ←嘘) Boolean: 真偽(「はい(True)」か「いいえ(False)」の二択) などがあります。 ちな「False」の読み方は「ファルス」ではなく「フォールス(フォルス)」です。 「NULL」は「ヌル」ではなく「ナル」です。だって「ヌル」だと「NULL NULL」の読み「ヌルヌル」じゃないですか... 読み方を間違えると意外と恥ずかしいので一応勉強しておいて損はないかもです。そんなことよりコードの勉強した方がいいんですけどね。羞恥心にはかなわねぇや。 ところで「ユーザーID」って言ってんのになんで文字列の「String」を使ってんだ?おめぇーって思った方。 (???) 良い質問ですね IDってなんか番号っぽいから数字の「Number」使えよってお思いでしょう。 実は以下の理由があります。 Stringのほうが情報量多め どんなフォーマットで対応できるように 文字列にする!って決めたほうが楽 ( 引用:https://qiita.com/sato-shin/items/0ec486d9c1bf98d9bf4c ) このようにすることでユーザープロフィールをどのようにデータベースに保存するかを決められました。 次に、このデータベースを実際に保存・使用していきたいと思います。 基本的にいつ保存するかというのは自由なんですが、私はコマンドを実行する「index.js」のinteractionCreate内の上部に書いています。 ※ちなみにinteractionCreate内の上部に書いても、実際に動作するのはコマンドを実行するときのみです。 index.js const profileModel = require('./models/profileSchema'); //先ほど作成したスキーマを参照 ~ ~ ~ client.on('interactionCreate', async interaction => { //メッセージを受け取ったら const profileData = await profileModel.findOne({ _id: interaction.user.id }); if (!profileData) { const profile = await profileModel.create({ _id: interaction.user.id, //ユーザーID name: interaction.user.username, //ユーザーネーム avatar: interaction.user.displayAvatarURL({ format: 'png' }), //アバター }); profile.save(); console.log('データベースに保存したよ: ' + interaction.user.tag); //一応ログとしてコンソールに出力 } if (!interaction.isCommand()) return; const command = client.commands.get(interaction.commandName); if (!command) return; //コマンド以外無視 }); ガチ気を付けてマジンコマジマジ async / await の非同期処理で行わないとエラーが発生せず、正常に動作しない可能性があります。これで僕は何度も苦しめられたんです!!!皆さんは気をつけましょうね!!!!!!絶対にな!!!絶対にダゾ!!!! <任意モデル>.create({ ナンチャラー }) で新しく作成できます。(設定によっては重複エラー発生するときありますので注意) <任意モデル>.findOne({ 検索 }) でその検索に当てはまるものを一つ取り出します。 <任意モデル>.findOneAndUpdate({ 検索 }, { ナンチャラー }) で検索に当てはまるものを取り出し、それにナンチャラーします。はい。 思ったより割と簡単でしょ? ということでこれで作成とかは完了しました。そしたら今度はコマンドで表示してみましょう。 今回は「profile.js」というJSファイルを作成し表示させてみます。 profile.js const profileModel = require('../models/profileSchema'); const { SlashCommandBuilder } = require('@discordjs/builders'); require('dotenv').config(); module.exports = { data: new SlashCommandBuilder() .setName('profile') .setDescription('指定したユーザーのプロフィールを表示します。') .addUserOption(option => option.setName('対象').setDescription('ユーザーを選択')), async execute(interaction) { const user = interaction.options.getUser('対象'); //コマンドオプションのユーザー選択から const profileData = await profileModel.findOne({ _id: user.id }); //ユーザーのIDでデータベースから合うものを一つ取り出す。 await interaction.reply(`ID: ${profileData.id}\nユーザーネーム: ${profileData.name}\nアバター: ${profileData.avatar}`); }, }; 上記のようなコードを記述することでプロフィールを表示するコマンドが作成できます。 これだけではまだ「ユーザーの情報を表示する」みたいなコマンドだけで充分ですが先ほどのスキーマをいろいろといじれば標準機能よりもはるかに便利で交流的なプロフィールを作成できるはずです! ぜひみなさんもプロフィール機能を自分なりに作ってみてください! 宣伝 私がメインで開発しているBOT「ヒトリン」。 ...使用してみませんか? 開発中でまだまだ「くそ便利すぎマジワロタ」とまではいかないですが、ぜひみなさんと成長していけるようなBOTを作成したいと思っています。 ぜひご検討ください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript関数ドリル】を毎日やる【勉強用】その5

【JavaScript関数ドリル】初級編のtake関数の実装のアウトプット take関数の挙動 _.take([1, 2, 3]); // => [1] _.take([1, 2, 3], 2); // => [1, 2] _.take([1, 2, 3], 5); // => [1, 2, 3] _.take([1, 2, 3], 0); // => [] 第2引数分の値を先頭から取得するもの take関数の課題内容 take関数に取り組む前の状態 前回作成したtakeRight関数をそのまま使用することにした take関数に取り組んだ後の状態 単純な機能でも様々な実装方法があるとわかった take関数の実装コード const take = (array, num) => { let newArray = []; if (num === 0) { return newArray; } else if (num > 1) { const sliceNumber = num; newArray = array.slice(0, sliceNumber); } else { newArray = array.slice(0, 1); } return newArray; }; console.log(take([1, 2, 3, 4, 5, 6], 3)); //[1,2,3] console.log(take([1, 2, 3, 4, 5, 6])); //[1] console.log(take([1, 2, 3, 4, 5, 6], 8)); //[1,2,3,4,5,6] console.log(take([1, 2, 3, 4, 5, 6], 0)); //[] take関数の解答コード function take(array, n = 1) { if(n === 0) { return []; } if(n > array.length) { return [...array]; } const takenValues = []; for(let i = 0; i < n; i++) { takenValues.push( array[i] ); } return takenValues; } console.log( take([1, 2, 3]) ); // => [1] console.log( take([1, 2, 3], 2) ); // => [1, 2] console.log( take([1, 2, 3], 5) ); // => [1, 2, 3] console.log( take([1, 2, 3], 0) ); // => []
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Apache EChartsを使ってグラフを書いてみる! ~1日10行コーディング~

概要 1日10行コーディングとは まず、1日10行コーディングというのは、1日10行でいいから新しいコード書こうよって感じのやつです(雑) (1日1記事とか1機能とかだと続かない気がしてしまってる...) 実装するもの とあるソシャゲのイベント走った記録とかができるサービスを作りたい。 今日はその1つの機能のグラフ表示部分を作ってみたいと思う。 手順 echartsのjsファイルをダウンロード https://www.jsdelivr.com/package/npm/echarts?path=dist こちらのページのdist配下のecharts.jsを保存する。 HTMLを記述 下記は公式にあるものを参考に記載 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="echarts.js"></script> <title>グラフ</title> </head> <body> <div id="main" style="width: 800px;height:400px;"></div> </body> </html> 上記のようにheadタグで1.で保存したecharts.jsを読み込むように定義して、グラフを表示したいエリアをdivで定義する。 JSでグラフの描画を記述 // Initialize the echarts instance based on the prepared dom var myChart = echarts.init(document.getElementById('main')); // Specify the configuration items and data for the chart var option = { title: { text: 'ECharts Getting Started Example' }, tooltip: {}, legend: { data: ['sales'] }, xAxis: { data: ['Shirts', 'Cardigans', 'Chiffons', 'Pants', 'Heels', 'Socks'] }, yAxis: {}, series: [ { name: 'sales', type: 'bar', data: [5, 20, 36, 10, 10, 20] } ] }; // Display the chart using the configuration items and data just specified. myChart.setOption(option); 開いてみる 上のタイトル部分とラベル部分がかぶってるが一旦表示はできた。 かぶらないように修正 場所がかぶってるのはwidthの値のせいだったから <div id="main" style="width: 800px;height:400px;"></div> こうすることで解消。 棒グラフにする 棒グラフにはオプション変えるだけでできた // Initialize the echarts instance based on the prepared dom var myChart = echarts.init(document.getElementById('main')); // Specify the configuration items and data for the chart var option = { title: { text: 'ECharts Getting Started Example' }, tooltip: { trigger: 'item' }, legend: { data: ['sales'] }, xAxis: { data: ['1/11', '1/12', '1/13', '1/14', '1/15', '1/16'] }, yAxis: {}, series: [ { name: 'sales', type: 'line', data: [1000, 2000, 3000, 3000, 5000, 6000] } ] }; // Display the chart using the configuration items and data just specified. myChart.setOption(option); 棒グラフできた!! 以上!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Chromeの恐竜をスクロールで再現してみた

はじめに 先日、痔の手術を行い数日間の入院生活を送っていたのですが、 入院期間中に何か作れないかなーと思って暇つぶしに作ってみたものです! Chromeの恐竜ゲームとは オフライン時にChromeブラウザで遊べるミニゲームです 完成したもの ページのスクロールに応じて恐竜が走ります ※ブラウザサイズによって挙動がちょっと不安定かもです?‍♂️ https://git-gen.github.io/dino_scroll.html 作り方 cssでドット絵を用意する 恐竜・サボテン・雲のcssを用意します cssでドット絵を描く方法は単純で、 基準とした要素に擬似要素のbox-shadowを使ってひたすらドットを打ちます ソースコード見て頂けるとわかると思うのですが、結構面倒なんでエクセルとか使うと楽です (例)■■□■ .dinosaur { position: relative; width: 40px; height: 10px; &::before { content: ''; position: absolute; width: 10px; height: 10px; transform: scale(0.3, 0.3); box-shadow: 0 0 $black, 10px 0 $black, 30px 0 $black; } } レイヤーを分けてパララックスを活用する レイヤー要素をキャラクター・背景・地面と分ける事で、キャラクターが前進しているしている様に見せます game_layer 恐竜とUIなど 画面固定 cloud_layer 雲 遅くスクロールする map_layer 地面・サボテン 速くスクロールする アニメーションを付与する アニメーションの付与にはjsの「GSAP」というライブラリを使用します scrollTriggerという機能をつかいアニメーションをつけるのですが、 GSAPの使用方法はざっくりとしか書いてません 詳しく知りたい方は公式の方を見ていただけると! https://greensock.com/docs/ 横スクロール 縦スクロールを横スクロールに変換します まずアニメーションにはxPercentを使用します xPercentはX軸方向に要素を動かすアニメーションです 次に重要なのはpinというオプションで、 これをtrueにするとアニメーション実行中は要素が画面に固定されます // 背景のスクロール gsap.to(".map_layer", { xPercent: -100, ease: "none", scrollTrigger: { trigger: ".container", pin: true, scrub: 1, start: "top top", end: "+2000", onUpdate: () => { dino_run(); } } }); // 雲のスクロール gsap.to(".cloud_layer", { xPercent: -40, ease: "none", scrollTrigger: { trigger: ".container", pin: true, scrub: 1, start: "top top", end: "+2000", } }); 恐竜のアニメーション 恐竜がサボテンをジャンプで避けるアニメーションをつけます 実際には恐竜がその場で跳ねているアニメーションをつけます gsap.toで恐竜が跳ねてgsap.fromToで恐竜が元の位置に戻ると行った感じ toggleClassを使うとアニメーション実行中に対象要素にclassを付与する事もできます gsap.to(".dinosaur", { y: -130, ease: "Power3.inOut", scrollTrigger: { trigger: ".game_layer", scrub: 1, start: "+250", end : "+300", toggleClass: { targets: ".dinosaur", className: "jump" }, } }); gsap.fromTo(".dinosaur", { y: -130, }, { y: 0, ease: "Power3.inOut", scrollTrigger: { trigger: ".game_layer", scrub: 1, start: "+300", end: "+350", toggleClass: { targets: ".dinosaur", className: "jump" }, } } ); その他 その他に恐竜の足をバタつきやゲームオーバーの表記はGSAPからのコールバックで行っています 使ったのはこちら onComplete アニメーションが完了した際に呼び出される onUpdate アニメーションが更新される度に呼び出される github 上記の手順で完成です! github pagesのソースはこちらになります おわりに 余談ですが、痔の治療はめちゃ痛かったです笑 どうしても座りっぱなしだとなりやすい&悪化しやすいみたいなので、 デスクワークのみなさんは気をつけてください!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Chromeの恐竜ゲームをスクロールで再現してみた

はじめに 先日、痔の手術を行い数日間の入院生活を送っていたのですが、 入院期間中に何か作れないかなーと思って暇つぶしに作ってみたものです! Chromeの恐竜ゲームとは オフライン時にChromeブラウザで遊べるミニゲームです 完成したもの ページのスクロールに応じて恐竜が走ります ※ブラウザサイズによって挙動がちょっと不安定かもです?‍♂️ https://git-gen.github.io/dino_scroll.html 作り方 cssでドット絵を用意する 恐竜・サボテン・雲のcssを用意します cssでドット絵を描く方法は単純で、 基準とした要素に擬似要素のbox-shadowを使ってひたすらドットを打ちます ソースコード見て頂けるとわかると思うのですが、結構面倒なんでエクセルとか使うと楽です (例)■■□■ .dinosaur { position: relative; width: 40px; height: 10px; &::before { content: ''; position: absolute; width: 10px; height: 10px; transform: scale(0.3, 0.3); box-shadow: 0 0 $black, 10px 0 $black, 30px 0 $black; } } レイヤーを分けてパララックスを活用する レイヤー要素をキャラクター・背景・地面と分ける事で、キャラクターが前進しているしている様に見せます game_layer 恐竜とUIなど 画面固定 cloud_layer 雲 遅くスクロールする map_layer 地面・サボテン 速くスクロールする アニメーションを付与する アニメーションの付与にはjsの「GSAP」というライブラリを使用します scrollTriggerという機能をつかいアニメーションをつけるのですが、 GSAPの使用方法はざっくりとしか書いてません 詳しく知りたい方は公式の方を見ていただけると! https://greensock.com/docs/ 横スクロール 縦スクロールを横スクロールに変換します まずアニメーションにはxPercentを使用します xPercentはX軸方向に要素を動かすアニメーションです 次に重要なのはpinというオプションで、 これをtrueにするとアニメーション実行中は要素が画面に固定されます // 背景のスクロール gsap.to(".map_layer", { xPercent: -100, ease: "none", scrollTrigger: { trigger: ".container", pin: true, scrub: 1, start: "top top", end: "+2000", onUpdate: () => { dino_run(); } } }); // 雲のスクロール gsap.to(".cloud_layer", { xPercent: -40, ease: "none", scrollTrigger: { trigger: ".container", pin: true, scrub: 1, start: "top top", end: "+2000", } }); 恐竜のアニメーション 恐竜がサボテンをジャンプで避けるアニメーションをつけます 実際には恐竜がその場で跳ねているアニメーションをつけます gsap.toで恐竜が跳ねてgsap.fromToで恐竜が元の位置に戻ると行った感じ toggleClassを使うとアニメーション実行中に対象要素にclassを付与する事もできます gsap.to(".dinosaur", { y: -130, ease: "Power3.inOut", scrollTrigger: { trigger: ".game_layer", scrub: 1, start: "+250", end : "+300", toggleClass: { targets: ".dinosaur", className: "jump" }, } }); gsap.fromTo(".dinosaur", { y: -130, }, { y: 0, ease: "Power3.inOut", scrollTrigger: { trigger: ".game_layer", scrub: 1, start: "+300", end: "+350", toggleClass: { targets: ".dinosaur", className: "jump" }, } } ); その他 その他に恐竜の足をバタつきやゲームオーバーの表記はGSAPからのコールバックで行っています 使ったのはこちら onComplete アニメーションが完了した際に呼び出される onUpdate アニメーションが更新される度に呼び出される github 上記の手順で完成です! github pagesのソースはこちらになります おわりに 余談ですが、痔の治療はめちゃ痛かったです笑 どうしても座りっぱなしだとなりやすい&悪化しやすいみたいなので、 デスクワークのみなさんは気をつけてください!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webの勉強はじめてみた その31 〜クライアントのフレームワーク〜

N予備校「プログラミング入門Webアプリ」を受講しています。 今回は第4章7,8節です。 モジュールバンドラー 複数ファイルのJavaScriptを1つにまとめられる Node.jsのコアモジュールをブラウザでも利用できる webpack モジュールバンドラーとしてwebpack を使用。 babel-loaderも同時にインストール。 yarn add webpack@4.26.1 webpack-cli@3.1.2 @babel/core@7.1.6 @babel/preset-env@7.1.6 babel-loader@8.0.4 --dev babel-loader - 最新のJavaScriptで書かれたコードをブラウザが実行できるバージョンにコンパイルするモジュール webpackの設定ファイル webpack.config.js module.exports = { context: __dirname + '/app', entry: './entry', output: { path: __dirname + '/public/javascripts', filename: 'bundle.js' }, mode: 'none', module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] } }; __dirname - 現在の場所を示す既に用意された変数(定数?) mode - bundle.jsの圧縮の方法 test - 正規表現を使うときの決まり文句 exclude - 外部ファイル。今回はnode_modulesから引っ張ってくる entry.js 'use strict'; import dc from 'damage-calc'; import crypto from 'crypto'; const root = document.getElementById('root'); root.innerHTML = `<p> 攻撃力 100, 防御 50, 防御貫通 30 のダメージは ${dc.effectiveDamage(100, 50, 30)} </p> <p> ${crypto.randomBytes(8).toString('hex')} </p> `; importで書かれたモジュールがbundle.jsに書き込まれる。 npx webpack webpack.configの内容をもとに、bundle.jsに出力。 expressで作られたlayout.pugを変更 layout.pug script(defer, src='/javascripts/bundle.js') defer属性 - スクリプトを文書の解析完了後に JavaScript を実行することをブラウザに示す属性。ページ表示速度が向上する。 jQuery HTML の文章の横断や操作、イベントハンドリング、アニメーションなどを 行うための簡単な API を提供するライブラリ インストール。 yarn add jquery@3.4.1 entry.js import $ from 'jquery'; const block = $('#block'); const scalingButton = $('#scaling-button'); scalingButton.click(() => { block.animate({ width: '200pt', height: '200pt' }, 2000); block.animate({ width: '100pt', height: '100pt' }, 2000); }); clickは古いらしい。 entry.js scalingButton.on('click', () => { block.animate({ width: '200pt', height: '200pt' }, 2000); block.animate({ width: '100pt', height: '100pt' }, 2000); }); まとめ この辺り、ちょっとサーバーとクライアントの境が自分の中で曖昧になってきてる。整理しないと。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

chart.jsのstepSizeはプロパティ名の小文字・大文字に注意

chart.jsの目盛り数が設定できない問題 chart.jsで5目盛りのレーダーチャートを作ろうとしてstepSizeプロパティをいじりまくるのだけれども、なぜか5目盛りにならない。目盛り量がすごいことになった状態のままうんともすんともいわない。 原因 めちゃくちゃ初歩的な原因でした。プロパティ名を正規表現「stepSize」にしていなかったことが原因。 それまで「StepSize」と頭文字を大文字にしてしまっていたために設定できていなかったようですー。 options: { scale: { ticks: { label: false, min: 0, max: 5, stepSize: 1, showLabelBackdrop: false, fontSize: 0, color: '#eee', }, gridLines: { display: true, borderWidth: 2, color: "#ddd" } }, scales: { xAxes: [{ display: true, stacked: false, gridLines: { display: false }, }] }, legend: { display: false }, tooltips: { enabled: false } } 以上のoptions記述で解決! 私はなかなかこれに気づかずハマっていたので、同じ悩みをもった方のもとに届きますように。 参考リンク chart.js 公式サイト:https://www.chartjs.org レーダーチャートのプロパティ設定に参考にしたサイト:http://www.kogures.com/hitoshi/javascript/chartjs/radar.html
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JavaScript]正しい配列のコピー

挨拶 Web系エンジニアへの就職に向け学習をしております、ひろやすと申します。 今回の記事は、JavaScriptの復習をしていて配列の正しいコピーの仕方を知ったので、学習のアウトプットとして投稿しようと思いました。 配列のコピーの仕方を知らない方の参考になれば幸いです。 正しい配列のコピー const array = ["ねずみ", "うし", "とら"]; // スプレッド構文を利用してカッコの中で配列の要素を取り出している const arrayCopy = [...array]; 間違った配列のコピー const array = ["ねずみ", "うし", "とら"]; // arrayを参照している const arrayCopy = array; // arrayの値が変更するとarrayCopyの値も変わってしまう array[0] = "いぬ" console.log(arrayCopy); // ["いぬ", "うし", "とら"] 筆者の悪い例 最後に、正しい配列のコピーの仕方を知らなかった以前の私が書いていたコードを載せておきます。 多分、こんなにまわりくどいコードを書く人はいないと思うけど...笑 const array = ["ねずみ", "うし", "とら"]; const data = []; // forEach文で要素を一つずつ配列に挿入している array.forEach((a) => { data.push(a); }; 最後までお読み頂きましてありがとうございます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

条件分岐について

条件分岐 条件分岐とは、条件に応じてプログラムの流れを切り替える仕組みのことです。 if文 if文は条件や値によって処理を分岐させ、一致する場合の処理と、一致しない場合の異なる処理を記述するための方法です。条件が一致する場合を「真(true)」、条件が一致しないことを「偽(false)」と言います。 sample1.js const num = 70; if (num > 80) { console.log("numは80より大きいです。"); }else{ console.log("numは80以下です。"); } //=> "numは80以下です。" else if 記述した条件が満たされた時に実行されます。 sample2.js const num = 70; if (num > 80) { console.log("numは80より大きいです。"); } else if (num >= 60) { console.log("numは60~80の間です。"); } else { console.log("numは60未満です。"); } // => "numは60~80の間です。" numの値が条件よりも満たしていなかったら。 sample3.js const num = 30; if (num > 80) { console.log("numは80より大きいです。"); } else if (num >= 60) { console.log("numは60~80の間です。"); } else { console.log("numは60未満です。"); } // => "numは60未満です。" どちらも満たされない場合は以下が実行されます。 == と === の違いについて sample.js const a = "1"; const b = 1; console.log(a==b); console.log(a===b); // => true // => false 等値演算子の評価結果は常に、比較が真かどうかに基づいて Boolean 型の値になります。 等値演算子は==で表現されます。 文字列と数値の比較の場合、文字列を数値に変換してくれます。 厳密等価演算子===で表現されます。 オペランド(数式を構成する要素のうち、演算の対象となる値や変数、定数などのこと)同士が、型を変換することなく、厳密に等しいならば真を返します。文字列は数値に変換されないためfalseになる。 複数の条件分岐も可能です。 演算子 > 大きい >= 以上 < 小さい <= 以下 == 等しい != 等しくない  比較演算子 比較演算子は2つの値が等しいか異なるかを評価し、true(真)もしくはfalse(偽)を返します。通常これらは論理式(論理結合子の適用対象となる記号列のこと)の中で使います。  真偽値 sample5.js const number = 25; console.log(number >= 1);//true 真偽値の条件式にはtrueかfalseしかありません。条件式が成立する場合はtrue、条件式が成立しない場合はfalseをコンソールは返します。 trueとみなされる値とfalseとみなされる値 falseとみなされる値 false 0 On ""(空文字) null undefined 数値の0や文字列の空文字、nullやundefinedなどはfalseとみなされます。 sample6.js if (0){ console.log('true'); }else{ console.log('false'); } //=> false trueとみなされる値 true 10 "mouth" [10, 20] 0以外の数値や空文字以外の文字列、他にオブジェクトは全てtrueとしてみなされます。 sample7.js if ('Yes'){ console.log('true'); }else{ console.log('false'); } if文の条件式にYesを記述しました。空文字以外の文字列はtrueとみなされます。 まとめ 第三者が見てもっとわかりやすいコードを心がけたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DrSmile社のウェブサイトを最適化し、最先端の実験を迅速に行えるようにする

DrSmileの急速な成長を後押しするために、迅速な実験を行える最適な環境を構築しました。 著者:Thom Krupa 2021年10月7日 原文:https://bejamas.io/blog/ab-testing-edge-case-study/ 1. DrSmileについて 2016年に設立されたDrSmileは、ヨーロッパ中で注目されている歯科会社です。彼らの使命は、歯科市場を民主化し、すべての人が自信を持って笑顔になれるようにすることです。彼らは、最新で手頃な価格の歯の矯正用透明アライナー技術という主力製品を通じて、それを実現してきました。 2. DrSmileとBejamas:Jamstackへの道 Bejamasのリード・デベロッパーの一人であるPatrycja Dzaluk氏は、DrSmileに関する素晴らしいケーススタディを書いています。その中で、DrSmileがどのようにして、現代的な歯科医療を提供する企業としての評判を確立したのか、ハイテクを駆使したアプローチや独自の専門知識の活用、最高の顧客サービスの提供にどのように力を注いだのかが紹介されています。 自分たちのマーケティングが自分たちの評判に合っているかどうかを確認し続けるためには、継続的にテストを行う必要があると考えたのです。そこで彼らは、成功した成長戦略を支え、強化するために、A/Bテスト戦略を導入したいと考えました。 3. A/B/nテストとは? A/Bテストとは、パフォーマンスに関する特定のKPIを測定するために、同じページの複数のバリエーションをテストする手法のことです。グロースマーケティングでは、実験を支援することが一般的です。A/Bテストの主な目的は、既存のウェブサイトや製品を継続的に改善し、同じようなページの異なるバージョンに対するユーザーの反応を知ることです。 例えば、簡単なヒーローのテキストと画像をテストして、全く異なる2つのバージョンのページを比較することができます。 A/Bテストを実行する最も簡単な方法は、Optimizely、VWO、Google Optimizeなど、このような実験を行うために設計されたアプリを使用することです。 さて、これらのツールを導入する際には、いくつかのルートがあります。そこで、A/Bテスト戦略を実施する際の主な課題について説明します。 3-1 A/Bテスト導入の課題とは? A/Bテストは、マーケターがマーケティングの成功を評価するための優れた手法ですが、そのためには導入戦略が必要です。そして、それぞれの導入戦略には、いくつかの副作用が伴います。 このような実験を行う最も簡単な方法を考えると、クライアントサイドのJSスクリプトをインストールすればいい、ということになります。そうすることで、各ツールがスニペットを生成し、テストしたいページの要素に入力してもらいます。 ここでの問題は、スタイルのちらつきを避けるために、スクリプトがレンダーブロックを行っていることです。つまり、スクリプトが読み込まれて実行されるまで、ブラウザは何もレンダリングできないということです。 ・テスト結果に影響を与える可能性のある深刻なパフォーマンス問題 ・読み込みが遅くなり、コンバージョン率が低下する ・ひどいユーザーエクスペリエンス それならば、サーバーサイド・エクスペリメントを使ってA/Bテストを実施しよう、ということになります。しかし、このオプションは、Jamstackをフル活用する場合には、私たちが持っていないサーバーを必要とします。言うまでもなく、スケーラビリティやキャッシングはサーバーにとって容易ではありません。 Jamstackを完全に活用するためには、できるだけユーザーの近くで実験を行い、最速の体験を保証したいと考えています。そして、ここからがEdgeの出番です。 Cloudflare WorkersやNetlify Edge Handlersなどのサーバーレス機能を、興味深い改良を加えて利用することができます。コールドスタートを最小限に抑えることができるので、ページ上での直接変更にも最適です。 4. Edgeとは? Edgeコンピューティングでは、ユーザーのすぐ近くでコードを実行することができます。この特性により、レイテンシーが減少し、レスポンスが向上します。標準的なセットアップでは、すべてのコードはオリジンサーバーまたはクラウドデータセンターで実行されます。エッジ関数の実行限界は50ミリ秒程度であることが多いため、Edgeは、小さなスクリプトを高速に実行したいときに最適な選択肢です。 一般的な使用例としては、以下のようなものがあります。 ・ユーザー認証。Edgeで直接ユーザーを確認することができます。 ・コンテンツのローカライズ。リクエストの場所に応じてコンテンツを翻訳します。 ・パーソナライズド広告 ・A/Bテスト 5. Edgeでのカスタマイズ DrSmileでは、Cloudflare Workersを使ってHTMLレスポンスを修正し、各ユーザーに異なるバリエーションの実験を提供することにしました。この応答は、当社がEdgeで生成し、cookiesに保存した匿名のユーザーIDに基づいています。 IDを取得したら、OptimizelyのAPIからデータを取得し、選択されたバリアントを表示します。 ここでは、新規ユーザーがウェブサイトにアクセスしたときの様子をご紹介します。 1.ユーザーIDがない場合は、ワーカーが新しい固有のIDを作成します。 2.作業者はOptimizelyに実験のために選択されたバリアントを求めます。 3.ワーカーは、HTMLを修正し、カスタマイズされたバージョンのページをユーザーに返します。 4.ユーザーは、cookieのヘッダーにIDを取得します。 リピーターの場合は、cookieに保存されているIDをすでに知っているので、最初のステップは省略できます。戻ってきたユーザーを検出すると、ワーカーは同じバリアントで戻ってきて、複数のセッションでテストの一貫性を保ちます。 6. エディターエクスペリエンスの変革 今こそ、エディター・エクスペリエンスを向上させる時です。ここでは、私たちが取り組んでいることを紹介します。DrSmileはヘッドレスCMSとしてContentfulを使用しており、OptimizelyはContentfulに接続するプラグインを用意しています。 そこで、編集者が簡単に実験できるように、プレビュー環境を作りました。 各コンポーネントにはシンプルなタブがあり、コントロールやバージョンを切り替えることができます。 7. 結果 大きな成果を得るためには、目的に応じた最適な戦略を立てていくことが必要です。DrSmile社は、顧客に最も洗練されたデジタル体験を提供したいと考えており、そのためにはJamstackを中心としたアーキテクチャが適していると考えました。 ユーザーフレンドリーなエディタを使用して、マーケティングチームは数分でA/Bテストを作成、実行することができます。顧客体験がいかに重要であるかを知っているので、素早くテストできることで、どのコンテンツや機能が顧客にとって最適であるかを自信を持って判断できるようになりました。 技術面では、Cloudflareのグローバルネットワークを活用できるCloudflare Workersを選択したことが成功の一因だと考えています。また、Cloudflare Workersへのデプロイは1秒以内にグローバルに伝播するため、ユーザーへの実験配信をスムーズに行う上で重要な役割を果たしています。軽量であるため、Cloudflare Workersはすべてのリクエストの前に実行することができ、ユーザーがどの実験を行うべきかを決定します。 7-1 コア・ウェブ・バイタルの修正 これらは過去30日間のデータです。全体像を見てみると、何ヶ月も前からパフォーマンスの問題に悩まされていました。 新しい設定は7月に導入され、それ以来、パフォーマンスが向上しています。8月は、DrSmileがCore Web Vitalsの評価に完全に準拠した最初の月です。 DrSmileのJosé Adameさんは次のように語っています。 クライアント側のA/Bテストアプリで発生するフリッカーに対処することは、これまでは、必要不可欠な苦痛でした。しかし、今はもうそんなことをしなくてよいのです。Optimizely Full StackとCloudflare Workers Edgeの技術を組み合わせることで、シームレスで高速なユーザーエクスペリエンスを実現するとともに、トラッキングやレポーティングを正確に行うことができます。 Alberto Adame DrSmileのシニア・プロダクト・マネージャー 私たちは、BejamasとDrSmileがコラボレーションできたことをとても嬉しく思います。そして、彼らのサイトがJamstackを使用してこれからも繁栄していくことを期待しています! 最後まで読んで下さり、ありがとうございました Jamstackに関心がある方はこちらまでお問合せください! 株式会社ヒューマンサイエンス https://www.science.co.jp/document/jamstack.html
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 03 ー 構成とロジックを整える

Recoil公式チュートリアルの解説は前回で終えました。この第3回はスピンオフです。とくに動きは変えることなく、モジュールの構成とロジックを整理します。せっかくモジュール分けもしましたので、もう少し実践に近づけようという試みです。 React + TypeScript: Recoil tutorial example 03 ディレクトリをコンポーネントと状態で分ける 簡単な作例ながら、それなりにモジュール数も増えました。そこで、コンポーネント(components/)と状態(state/)を、つぎのようにディレクトリ分けすることにします。 src/components/ App.tsx TodoItem.tsx TodoItemCreator.tsx TodoList.tsx TodoListFilters.tsx TodoListStats.tsx src/state/ filteredTodoListState.ts todoListFilterState.ts todoListState.ts todoListStatsState.ts すると、各モジュールがimportするパスを修正しなければなりません。機械的な書き替えですので、コードを羅列します。動きが変わらず、エラーの出ないことを確かめてください。 src/index.tsx // import App from './App'; import App from './components/App'; src/components/TodoItem.tsx // import { todoListState, TodoItemType } from './todoListState'; import { todoListState, TodoItemType } from '../state/todoListState'; src/components/TodoItemCreator.tsx // import { todoListState } from './todoListState'; import { todoListState } from '../state/todoListState'; src/components/TodoList.tsx // import { filteredTodoListState } from './filteredTodoListState'; import { filteredTodoListState } from '../state/filteredTodoListState'; src/components/TodoListFilters.tsx // import { todoListFilterState } from './todoListFilterState'; import { todoListFilterState } from '../state/todoListFilterState'; src/components/TodoListStats.tsx // import { todoListStatsState } from './todoListStatsState'; import { todoListStatsState } from '../state/todoListStatsState'; フィルタ設定のロジックをコンポーネントから状態に移す フィルタの設定はコンポーネント(TodoListFilters)から行うのでなく、状態(todoListFilterState)にカスタムフック(useFilter)として移します。コードはむしろ増えるものの、フックを介すことによりコンポーネントが直に状態を変えることがなくなるのです。そのため、コンポーネントが用いるRecoilのフックも、値を参照するだけのuseRecoilValueに差し替えました。 src/state/todoListFilterState.ts import { useCallback } from 'react'; // import { atom } from 'recoil'; import { atom, useSetRecoilState } from 'recoil'; export const useFilter = () => { const setFilter = useSetRecoilState(todoListFilterState); const setListFilter = useCallback( (filter: string) => { setFilter(filter); }, [setFilter] ); return { setListFilter }; }; src/components/TodoListFilters.tsx // import { useRecoilState } from 'recoil'; import { useRecoilValue } from 'recoil'; // import { todoListFilterState } from '../state/todoListFilterState'; import { todoListFilterState, useFilter } from '../state/todoListFilterState'; export const TodoListFilters: VFC = () => { // const [filter, setFilter] = useRecoilState(todoListFilterState); const filter = useRecoilValue(todoListFilterState); const { setListFilter } = useFilter(); const updateFilter: ChangeEventHandler<HTMLSelectElement> = useCallback( ({ target: { value } }) => { // setFilter(value); setListFilter(value); }, // [setFilter] [setListFilter] ); }; Todoリストへの項目データ追加をコンポーネントから状態に移す Todoリストに項目を追加するコンポーネント(TodoItemCreator)のロジックも、同じように状態(todoListState)のフック(useTodoList)に移します。すると、コンポーネントは状態を直に参照することさえなくなるのです。 src/state/todoListState.ts import { useCallback } from 'react'; // import { atom } from 'recoil'; import { atom, useSetRecoilState } from 'recoil'; let id = 0; function getId() { return id++; } export const useTodoList = () => { const setTodoList = useSetRecoilState(todoListState); const addListItem = useCallback( (text: string) => { setTodoList((oldTodoList) => [ ...oldTodoList, { id: getId(), text, isComplete: false, }, ]); }, [setTodoList] ); return { addListItem }; }; src/components/TodoItemCreator.tsx // import { useSetRecoilState } from 'recoil'; // import { todoListState } from '../state/todoListState'; import { useTodoList } from '../state/todoListState'; export const TodoItemCreator: VFC = () => { // const setTodoList = useSetRecoilState(todoListState); const { addListItem } = useTodoList(); const addItem = useCallback(() => { /* setTodoList((oldTodoList) => [ ...oldTodoList, { id: getId(), text: inputValue, isComplete: false, }, ]); */ addListItem(inputValue); // }, [inputValue, setTodoList]); }, [addListItem, inputValue]); }; /* let id = 0; function getId() { return id++; } */ Todoリストの項目編集・チェック・削除の操作をコンポーネントでなく状態に備える Todoリストの各項目を編集・チェック・削除するTodoItemは、もっともロジックの多いコンポーネントです。操作を順に状態(todoListState)に移しましょう。 Todoリストの項目をフックで編集する まず、Todoリスト項目の編集です。コンポーネント(TodoItem)から状態(todoListState)のフック(useTodoList)に移します。カスタムフックから状態の設定だけでなく参照もできるように、RecoilのuseRecoilStateを用いなければなりません。 src/state/todoListState.ts // import { atom, useSetRecoilState } from 'recoil'; import { atom, useRecoilState } from 'recoil'; let id = 0; function getId() { return id++; } function replaceItemAtIndex( arr: TodoItemType[], index: number, newValue: TodoItemType ) { return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]; } export const useTodoList = () => { // const setTodoList = useSetRecoilState(todoListState); const [todoList, setTodoList] = useRecoilState(todoListState); const editItemTextAtIndex = useCallback( (index: number, item: TodoItemType, text: string) => { const newList = replaceItemAtIndex(todoList, index, { ...item, text, }); setTodoList(newList); }, [setTodoList, todoList] ); // return { addListItem }; return { addListItem, editItemTextAtIndex }; }; src/components/TodoItem.tsx // import { todoListState, TodoItemType } from '../state/todoListState'; import { todoListState, TodoItemType, useTodoList, } from '../state/todoListState'; export const TodoItem: VFC<Props> = ({ item }) => { const { editItemTextAtIndex } = useTodoList(); const editItemText: ChangeEventHandler<HTMLInputElement> = useCallback( ({ target: { value } }) => { /* const newList = replaceItemAtIndex(todoList, index, { ...item, text: value, }); setTodoList(newList); */ editItemTextAtIndex(index, item, value); }, // [index, item, setTodoList, todoList] [editItemTextAtIndex, index, item] ); }; Todo項目のチェックを切り替える つぎは、Todo項目のチェックの切り替えです。コンポーネント(TodoItem)から状態(todoListState)のフック(useTodoList)に関数(toggleItemCompletionAtIndex)を移します。 src/state/todoListState.ts export const useTodoList = () => { const toggleItemCompletionAtIndex = useCallback( (index: number, item: TodoItemType) => { const newList = replaceItemAtIndex(todoList, index, { ...item, isComplete: !item.isComplete, }); setTodoList(newList); }, [setTodoList, todoList] ); // return { addListItem, editItemTextAtIndex }; return { addListItem, editItemTextAtIndex, toggleItemCompletionAtIndex }; }; src/components/TodoItem.tsx export const TodoItem: VFC<Props> = ({ item }) => { const toggleItemCompletion = useCallback(() => { /* const newList = replaceItemAtIndex(todoList, index, { ...item, isComplete: !item.isComplete, }); setTodoList(newList); */ toggleItemCompletionAtIndex(index, item); // }, [index, item, setTodoList, todoList]); }, [index, item, toggleItemCompletionAtIndex]); }; /* function replaceItemAtIndex( arr: TodoItemType[], index: number, newValue: TodoItemType ) { return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]; } */ Todoリストから項目を削除する 最後は、Todoリストからの項目の削除です。コンポーネント(TodoItem)のロジックを、状態(todoListState)のフック(todoListState)に移します。もはやコンポーネントから状態を書き替える必要がありません。用いるRecoilのフックは、読み取り専用のuseRecoilValueに改めました。これで、すべてのコンポーネントからRecoilの状態は参照するのみで、値を直に書き替えることはなくなったのです。 src/state/todoListState.ts export const useTodoList = () => { const deleteItemAtIndex = useCallback( (index: number) => { const newList = removeItemAtIndex(todoList, index); setTodoList(newList); }, [setTodoList, todoList] ); // return { addListItem, editItemTextAtIndex, toggleItemCompletionAtIndex }; return { addListItem, deleteItemAtIndex, editItemTextAtIndex, toggleItemCompletionAtIndex, }; }; src/components/TodoItem.tsx // import { useRecoilState } from 'recoil'; import { useRecoilValue } from 'recoil'; export const TodoItem: VFC<Props> = ({ item }) => { // const [todoList, setTodoList] = useRecoilState(todoListState); const todoList = useRecoilValue(todoListState); // const { editItemTextAtIndex, toggleItemCompletionAtIndex } = useTodoList(); const { deleteItemAtIndex, editItemTextAtIndex, toggleItemCompletionAtIndex, } = useTodoList(); const deleteItem = useCallback(() => { /* const newList = removeItemAtIndex(todoList, index); setTodoList(newList); */ deleteItemAtIndex(index); // }, [index, setTodoList, todoList]); }, [deleteItemAtIndex, index]); }; /* function removeItemAtIndex(arr: TodoItemType[], index: number) { return [...arr.slice(0, index), ...arr.slice(index + 1)]; } */ フィルタの値の定数化と型定義 フィルタに設定する値は、3つの文字列だけです。だとすれば、定数にしてしまえると、管理しやすくスペルミスも防げます。ところが、標準JavaScriptでconst宣言したオブジェクトは、上書きができないだけです。それぞれのプロパティ値は書き替えられてしまいます。そいうとき、オブジェクトにconstアサーションを加えると、各プロパティも読み取り専用になるのです(「constアサーション「as const」 (const assertion)」参照) src/state/todoListFilterState.ts const FilterValue = { SHOW_ALL: 'Show All', SHOW_COMPLETED: 'Show Completed', SHOW_UNCOMPLETED: 'Show Uncompleted' } as const; さらに、3つの値しかとれない型が定められるならより安全になります。TypeScriptの演算子keyofとtypeofを組み合わせれば、そのようなユニオン型がつくれるのです(「オブジェクトからキーの型を生成する」および「TypeScriptの『typeof X[keyof typeof X]』の意味を順を追って理解する」参照)。 src/state/todoListFilterState.ts type FilterType = typeof FilterValue[keyof typeof FilterValue]; // つぎのユニオン型になる // type FilterType = 'Show All' | 'Show Completed' | 'Show Uncompleted' 改めて、フィルタの状態のモジュール(src/state/todoListFilterState.ts)は、つぎのように書き直します。 src/state/todoListFilterState.ts export const FilterValue = { SHOW_ALL: 'Show All', SHOW_COMPLETED: 'Show Completed', SHOW_UNCOMPLETED: 'Show Uncompleted' } as const; export type FilterType = typeof FilterValue[keyof typeof FilterValue]; // export const todoListFilterState = atom<string>({ export const todoListFilterState = atom<FilterType>({ // default: 'Show All', default: FilterValue.SHOW_ALL, }); export const useFilter = () => { const setListFilter = useCallback( // (filter: string) => { (filter: FilterType) => { }, ); }; フィルタを選択するコンポーネントも、importした型(FilterType)と値(FilterValue)を使って書き改めます。フィルタの状態に設定する(setListFilterの引数)値の型は、これまでのstringでは合わなくなりましたので、FilterTypeで型アサーションしなければなりません。 src/components/TodoListFilters.tsx // import { todoListFilterState, useFilter } from '../state/todoListFilterState'; import { FilterType, FilterValue, todoListFilterState, useFilter, } from '../state/todoListFilterState'; export const TodoListFilters: VFC = () => { const updateFilter: ChangeEventHandler<HTMLSelectElement> = useCallback( ({ target: { value } }) => { // setListFilter(value); setListFilter(value as FilterType); }, [setListFilter] ); return ( <> <select value={filter} onChange={updateFilter}> {/* <option value="Show All">All</option> */} <option value={FilterValue.SHOW_ALL}>All</option> {/* <option value="Show Completed">Completed</option> */} <option value={FilterValue.SHOW_COMPLETED}>Completed</option> {/* <option value="Show Uncompleted">Uncompleted</option> */} <option value={FilterValue.SHOW_UNCOMPLETED}>Uncompleted</option> </select> </> ); }; もうひとつだけ、Todoリスト項目がみずからを特定するために用いるキーのindexです。これは、useMemoフックでメモ化しておく方がよいでしょう。 src/components/TodoItem.tsx // import { ChangeEventHandler, useCallback, VFC } from 'react'; import { ChangeEventHandler, useCallback, useMemo, VFC } from 'react'; export const TodoItem: VFC<Props> = ({ item }) => { // const index = todoList.findIndex((listItem) => listItem === item); const index = useMemo( () => todoList.findIndex((listItem) => listItem === item), [item, todoList] ); }; フィルタの値は3つにかぎられ、その型も定められました。他のモジュールからの扱いがしやすくなり、安全になったでしょう。でき上がったTodoリストアプリケーションの各モジュールのコードは、冒頭に掲げたCodeSandboxのサンプルをご参照ください。 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 01 ー atomを使った項目操作」」 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 02 ー selectorによるフィルタリングと集計」
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】reduce()で配列から指定した値の近似値を取得する

海外のブログで、配列から指定した値の近似値を取得する方法が紹介されていたのでメモ。いったん配列をソートしてから近似値を求めるより、reduce() を使った方が早いらしい。 参考: JFind the closest number in an array JavaScript 配列内で "8" に一番近い値を取得する const needle = 8; const closest = [1, 10, 7, 2, 4, 9].reduce((a, b) => { return Math.abs(b - needle) < Math.abs(a - needle) ? b : a; }); console.log(closest); // 出力値: 7 この方法だと結果は "7" になる。配列に "9" があったとしても、先に比較された "7" が最近似値となる。 これだと問題となるケースがあるため、「大きい方 vs 小さいほう (> vs <)」を指定して、どちらを近似値とするかを決められるようにする。 const needle = 8; const closest = [1, 10, 7, 2, 4, 9].reduce((a, b) => { let aDiff = Math.abs(a - needle); let bDiff = Math.abs(b - needle); if (aDiff == bDiff) { // 大きい方 vs 小さいほう (> vs <) を指定する return a > b ? a : b; } else { return bDiff < aDiff ? b : a; } }); console.log(closest); // 出力値: 9 使い回しができるよう関数化する function getClosestNum(needle, haystack) { return haystack.reduce((a, b) => { let aDiff = Math.abs(a - needle); let bDiff = Math.abs(b - needle); if (aDiff == bDiff) { // 大きい方 vs 小さいほう (> vs <) を指定する return a > b ? a : b; } else { return bDiff < aDiff ? b : a; } }); } ざっと動作確認してみる。 // 数値のみの配列 const needle = 8; const numbers = [1, 52, 0, -25, 10, 7, 2, -128, 4, 9, 99, 256]; let closetnum = getClosestNum(needle, numbers); console.log(closetnum); // 出力値: 9 // いろいろごちゃまぜの配列 const needle = 0.5940; const numbers = [1, 10, NaN, 7, -5, null, "テスト", 2, 4, 9, "Test", 0.5267, 0.5877, NaN, 0.6534, NaN]; let closetnum = getClosestNum(needle, numbers); console.log(closetnum);// 出力値: 0.5877 いろいろごちゃまぜの配列にしても、近似値を取得できているが、あまり無茶をするとバグりそう(汗 参考: JFind the closest number in an array JavaScript JavaScript 配列から指定した値の近似値を取得 【javascript】reduce
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ストック推奨】Webフロントエンドパフォーマンスチューニング55選

こんにちは、ぬこすけです。 近年、Webフロントエンドではサイトのパフォーマンスの重要性が高まっています。 例えば、GoogleはCore Web Vitalというパフォーマンスに指標を検索結果のランキング要因に組み込みました。 また、近年の某企業が「パフォーマンスの改善に取り組んだ結果、セッション数〇%アップ、CVR〇%アップ...」などの事例は枚挙にいとまがないでしょう。 パフォーマンスチューニングするためには、定量的に計測してボトルネックを探すようなトップダウンなアプローチもあります。 しかしながら、時には千本ノック的にハウツーを片っ端から試していくボトムアップなアプローチも有効になることもあったり、日々のコーディングでパフォーマンスを意識したコードを書くことは大切でしょう。 この記事ではパフォーマンス最適化のハウツーを紹介します。 パフォーマンス改善の施策が思い浮かばない時やフロントエンドのスキルを磨きたい時に辞書的な役割を果たせれば良いかなーと思っています。 ※私は55選書いている所で燃え尽きました。私自身も今後も更新する予定ですが、この記事は皆さんで作り上げたいと思っています。誤りや他にもこういうのあるよ!、この記事にわかりやすい説明あるよ!などあれば編集リクエスト、またはコメントいただけると幸いです。 ※この記事を読んでいる方にはこれからフロントエンジニアになりたい方、駆け出しエンジニアの方もいると思います。正直、何言ってるかわからない部分が結構あると思います。ですが、私の経験則上、「あの時書いてあったことはこういうことか!」と後々になって理解することがよくありました。今はよくわからないかもしれませんが、とりあえずストックなりしておいて、数ヶ月後にこの記事を見返すとまた理解度も変わるのかなーと思います。 注意事項 一口にフロントエンドといっても、SSRやらSSGやらでサーバー側も関わってくることもあるので、バックエンド寄りも話も混じっているので悪しからず。 わかりやすくするためにカテゴリに分けしていますが、微妙なカテゴリ分けのものもあるので悪しからず。 中には具体的なハウツーというより考え方みたいなものも混じっているかもしれませんが悪しからず。 環境によって必ずしもパフォーマンスが改善されるとは限らないので悪しからず。 あくまでパフォーマンスの観点なので他の観点では最適となるとは限らないので悪しからず。例えば、IndexedDBを紹介していますが、Sarafi 15で脆弱性が見つかっています。 紹介するものには特定のブラウザでしかサポートされていないものもあるので悪しからず。 JavaScript編 複数の非同期処理はPromise.allを使う もし互いに依存関係のない複数の非同期処理を実行しているのならば、Promise.allを使うのも手です。 async function notUsePromiseAll() { console.log('Start!!'); const response1 = await fetch("https://example.com/api/1"); const response2 = await fetch("https://example.com/api/2"); console.log('End!!'); } async function usePromiseAll() { console.log('Start!!'); const [response1, response2] = await Promise.all([ fetch("https://example.com/api/1"), fetch("https://example.com/api/2"), ]); console.log('End!!'); } Promise.allはいずれかの非同期処理が失敗すると全ての非同期処理が中断されます。 中断されたくない場合はPromise.allSettledが使えます。 非同期処理を待たなくて良い場合は待たない コードを眺めてみて、非同期処理を待たなくて良いところは待たないようにしましよう。 具体的には、もしasync/await構文を使っているならawaitを使わないことです。 const sendErrorToServer = async (message) => { // サーバーにエラー情報を送る処理 }; console.log('何かエラーが起きた'); // 後続の処理はサーバーにエラー情報を送る処理とは関係ないので await をつけない sendErrorToServer('エラーです'); console.log('後続の処理'); 先に非同期処理を走らせておく 互いに依存関係のある複数の非同期処理を実行する場合でも、時間がかかる処理の方を先に走らせておくのも良いでしょう。 const response1Promise = requestLongTime(); // ... // 色々処理 // ... const response1 = await response1Promise; const response2 = await requestShortTime(); console.log(response1, response2); キー/バリューを頻繁に追加や削除する場合はMapを使う MDNにも記載がありますが、キー/バリューのペアを頻繁に追加や削除する場合はObjectよりもMapを使ったほうが最適です。 const nameAgeMap = new Map() nameAgeMap.set('Tom', 19) nameAgeMap.set('Nancy', 32) nameAgeMap.delete('Tom') nameAgeMap.delete('Nancy') ... 膨大な配列の検索はキー/バリューで JavaScriptというよりかはロジックの問題かもしれません。 膨大な配列を検索する場合はキー/バリューに変換してから検索した方が速いです。 const thousandsPeople = [ { name: 'Tom', age: 19 }, { name: 'Nancy', age: 32 }, // ...めちゃくちゃ多い ] // 時間かかる const myFriend = thousandsPeople.find(({ name }) => name === 'Tom'); console.log(`The age is ${myFriend.age}`); const thousandsPeopleMap = { 'Tom': 19, 'Nancy': 32, // ... } // こっちのほうが速い const myFriend2 = thousandsPeopleMap['Tom']; console.log(`The age is ${myFriend2.age}`); 関数の結果をキャッシュする 頻繁に同じ引数で関数を実行したり、重い処理を走らせるなら関数の結果をキャッシュするのも有効です。 次のようなデコレータ関数を作れば、関数の結果をキャッシュできます。 function cachingDecorator(func) { const cache = new Map(); return x => { if (!x) { return func(x) } if (cache.has(x)) { return cache.get(x); } const result = func(x); cache.set(x, result); return result; } } function heavyFuncNoCache(str) { // 重い処理 } const heavyFunc = cachingDecorator(heavyFuncNoCache); heavyFunc('hoge'); // キャッシュから結果が返却される heavyFunc('hoge'); requireではなくimportを使う JavaScriptのモジュールの読み込み方にはrequireとimportの2種類があります。 requireは同期的、importは非同期的にモジュールを読み込むので、importの方が良いでしょう。 Node.jsといったサーバーサイドでJavaScriptを記述する場合はrequireを使うことが多いと思いますが、バージョン14であればpackage.jsonだったりファイルの拡張子をmjsにしたりいじることでimportで読み込めます。 なお、Qiitaのこの記事がわかりやすいです。 フェッチにはKeep-Aliveを指定する 何度も同じドメインへアクセスするのであればkeep-aliveを指定することでフェッチ処理が短縮されます。 import axios from 'axios'; import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; const httpAgent = new HttpAgent({ keepAlive: true }); const httpsAgent = new HttpsAgent({ keepAlive: true }); const keepAliveAxios = axios.create({ httpAgent, httpsAgent, }); keepAliveAxios.get(...); 非同期の関数を使う Node.jsには同期/非同期で別で用意されている関数があったりします。 例えばファイルに書き込みをする関数にはfs.writeFileSyncとfs.writeFileがあります。 もしフロントエンドアプリケーションのビルド時などに静的ファイルを生成する必要がある場合、特段理由がなければfs.writeFileを使いましょう。 HTML/CSSなどリソース編 imgやiframe、linkタグなどにimportance属性を追加する imgやiframe、linkタグなどではimportance属性を使うことでブラウザに読み込みの優先度を指定できます。 タグだけでなくfetch関数でもオプションでimportanceを指定できたりします。 imgやiframeタグにloading属性を追加する imgやiframeタグにはloading属性を使うことで読み込みのタイミングを指定できます。 もし、遅延/非同期読み込みしたい場合はloading='async'を使うと良いでしょう。 ただし、ファーストビューに使うと返って読み込みが遅くなる可能性もあるので注意しましょう。 imgタグにdecoding属性を追加する imgタグはdecoding属性を使うことでデコードを同期/非同期的に読み込むかを指定できます。 decoding='async'を指定すれば非同期的にデコード処理をブラウザに指示できます。 imgタグにはサイズを指定しておく imgタグのwidth/height属性などを使って、画像のサイズを指定しておきましょう。ブラウザのレンダリングの助けになります。 CLSの改善にも繋がります。 わからない場合は大体のサイズを指定しましょう。 優先度の高いリソースはlinkタグにpreloadを指定する ファーストビューに表示する画像など、優先度の高いリソースはlinkタグのrel属性にpreloadを指定ことで速い読み込みが期待できます。 優先度の高い外部ドメインへのアクセスがある時はlinkタグにdns-prefetchまたはpreconnectを指定する 外部ドメインからリソースを取得したり重要度の高い外部リンクを設置している場合などは、linkタグのdns-prefetchやpreconnectが使えます。 dns-prefetchはDNSルックアップ、preconnectは事前接続まで行います。 かなり優先度の高い外部ドメインへのアクセスはpreconnect、少し優先度が落ちる場合はdns-prefetchを使うと良いでしょう。 ユーザーがよく遷移するページはlinkタグにprerenderを指定する linkタグのrel属性にprerenderを指定することで、ブラウザは指定されたページをバック グラウンドでレンダリングします。 なので、ユーザーが指定されたページへ遷移する時はすぐに画面表示ができます。 ユースケースとしては、ランキングサイトのようなページで1位へのページへ遷移するユーザーは多いので、prerenderを指定しておくと良いかもしれません。 ただし、レンダリングされる都合上、ブラウザへの負荷が高かったり、JavaScriptで仕込んでいる計測処理が発火するなどの注意は必要です。 scriptタグにdeferやasync属性を追加する ブラウザでスクリプトが読み込まれるとHTMLやCSSの解析がブロックされます。 このような問題を解決するためにdeferやasync属性が使えます。 deferはHTMLやCSSの解析をブロックすることなくスクリプトを読み込んでおき、解析が完了したらスクリプトを実行します。 asyncはHTMLやCSSの解析とは独立してスクリプトの読み込み・実行をします。 Qiitaのこの記事がわかりやすいです。 優先度の高いリソースの読み込みはできるだけHTML上部で定義する ブラウザはHTMLドキュメントの上から解釈してきます。 なので、例えば同じpreloadを指定しているリソースでも、さらに優先度の高いものはよりHTML上部に定義して早めにブラウザが読み込めるようにしましょう。 CSSで余計なセレクタは書かない ブラウザはCSSセレクタを右から左に解析します。 なので、できる限り単一のクラス名やid名で指定した方が解析のスピードが上がります。 /* ブラウザは全てのdivタグを探し、さらに上の階層のhogeクラスを見つけようと解析する */ .hoge div {} /* Best Practice */ .hoge {} #hoge {} style属性を使って直接スタイルを指定する クラスなどセレクタを指定してCSSを書くよりも、直接HTMLタグのstyle属性を使ったほうがブラウザの解析は速いです。 ただし、コードの可読性やメンテが厳しくはなります。 <div style='color: red;'>ほげ</div> 不要なCSSを削除する 使っていないCSSは削除しましょう。 Chromeのデベロッパーツールを使えば不要なCSSを洗い出すことができます。 不要なJavaScriptを削除する 使っていないJavaScriptは削除しましょう。 例えば、console.logは基本的にプロダクションのコードでは不要なので、eslintで検出するなりbabelで削除するなりします。 ファーストビューに影響のあるCSSはheadタグの先頭で読み込む JavaScriptと違い、ブラウザのCSSの解析はHTMLの解析をブロックしません。 ファーストビューで読み込ませたいCSSはできるだけheadタグの先頭に読み込ませて、速くスタイリングされたファーストビューをユーザーに見せるようにしましょう。 ファーストビューに影響のないCSSはbodyタグの末尾で読み込む 逆にファーストビューに影響のないCSSはbodyタグの末尾で読み込ませることで、ブラウザにCSSの読み込みを遅延させます。 JavaScriptはbodyタグの末尾で読み込む ブラウザはJavaScriptの解析を始めるとHTMLやCSSの解析をストップします。 なので、JavaScriptはbodyタグの末尾で読み込み、HTMLやCSSの解析が終わった後のJavaScriptを解析するようにしましょう。 ただし、Google Analyticsなどの解析用のJavaScript等は除きます。 HTMLやCSS、JSをMinify/バンドルする Webpackやswcなどのバンドラーを使いましょう。 JavaScriptのトランスパイルを最新のESに合わせる もしJavaScriptをES2015でトランスパイルしている場合は、それよりも最新のバージョンでトランスパイルすることによって、JavaScriptのサイズを落とすことができます。 ただし、IEといった古いブラウザを切り捨てる覚悟は必要です。 画像はWebPやAVIFを使う 次世代の画像フォーマットとしてWebPやAVIFがあります。 こららの画像フォーマットを使うことで従来のPNG等の形式よりも画像サイズを縮小できたりします。 IKEAではAVIFによって画像の転送量を21.4%削減した例もあります。 画像サイズを縮小する 画質を落とすなり幅/高さを小さくするなりして画像サイズを縮小させます。 例えば、SVGでは作成したツールによってはコメントアウトが残っていたりで最適化されずに出力されている場合もあるので、手動で削除するなりツールを使うなりで縮小させます。 画像をインライン化する インライン画像としてHTMLに直接埋め込むことで、画像のリクエスト数を抑えることができます。 ただし、画像サイズが大きくなったりブラウザのキャッシュが効かない等のデメリットはあります。 画像サイズが小さく、一度しか読み込まれない場合などに有効といわれています。 過大なDOMを避ける DOMが多すぎるとブラウザの描画に負担をかけてしまいます。 不要なDOMを削除するのはもちろん、遅延読み込みや仮想無限スクロールなどを駆使してユーザーに表示されている部分だけ描画することで対策できます。 ブラウザAPI編 永続化ストレージはLocalStorageよりIndexedDBを使う ブラウザの永続化ストレージにはLocalStorageとIndexedDBが使えます。 LocalStorageは同期的、IndexedDBは非同期処理なので、IndexedDBの方がブラウザの動きを阻害することなくデータアクセスができます。 重たい処理やUIに依存しない処理はWebWorkerを使う WebWorkerを使うことでブラウザのメインスレッドとは別のスレッド立ち上げることができます。 フロントで検索機能といった重たい処理だったり、エラーをサーバーに送信するといったUIに依存しない処理はWebWorkerを使うことでメインスレッドの処理を阻害させません。 ServiceWorkerでリソースをキャッシュする ServiceWorker といえばPWA(Progressive Web Application)のイメージが強いですが、ブラウザから外部サーバーへのリクエストをフックしてHTMLやCSS、JSなどのリソースをキャッシュすることができます。 リクエストする際はキャッシュから取得することができるので外部サーバーへのリクエストするよりも処理が速くなります。 また、キャッシュから取得するか、先にサーバーへデータ取得してからキャッシュするかなど柔軟なキャッシュ戦略を選択できます。 ServiceWorkerを使う時はNavigationPreloadsも使う サイトにアクセス時、必要なリソースをフェッチする時にはServiceWorkerが起動するのを待ってフェッチ処理が走ります。 NavigationPreloadsではServiceWorkerの起動を待たずフェッチ処理を開始することができます。 WebAssembly を使う JavaScriptだけでなく、CやRustで書いたコードがブラウザで実行でき、JavaScriptよりも高速化される場合があります。 Amazonの事例もあります。 V8エンジン編 ChromeやNode.jsでは内部的にV8エンジンが使われています。 ここまで最適化すると変態ですが、チップスとして紹介します。 値の格納はコンストラクタで V8エンジンでは内部的にhidden classというものを生成します。 詳しい仕組みは割愛しますが、インスタンス化したオブジェクトに対して値を追加すると、新しいhidden classが生成されてしまいます。 class Point { constructor(x, y) { this.x = x; this.y = y; } } var p1 = new Point(11, 22); // hidden class の生成 var p2 = new Point(33, 44); // hidden class の再利用 p1.z = 55; // hidden class が生成されてしまう オブジェクトは同じ順番のプロパティで生成する これもhidden classに関わる話ですが、違う順番でプロパティを生成すると新たにhidden classが生成されます。 const obj = { a: 1 }; obj.b = 2 // hidden classを使い回せる const obj2 = { a: 1 }; obj2.b = 2 // 新しいhidden classが生成されてしまう const obj3 = { b: 2 }; obj3.a = 1 関数は同じ引数の型を使う 関数の引数はできるだけ同じ型を使うようにします。 function add(x,y) { return x + y } add(1,2) add(3,4) // OK add('3','4') // NG クラスはトップレベルのスコープで定義する 関数内でクラスを定義するのはV8エンジン的には良くないそうです。 // NG function createPoint(x, y) { class Point { constructor(x,y) { this.x = x this.y = y } } return new Point(x,y) } ライブラリ編 軽量なライブラリを採用する ライブラリを採用する1つの観点としてサイズがあります。 bundlephobia というサイトでライブラリのサイズをチェックすることができます。 ライブラリのサイズを減らす moment.js や lodash などのライブラリはWebpackのプラグインを使って不必要なスクリプトを削減することができます。 ライブラリのドキュメントを読む ライブラリの公式ドキュメントには最適化のTipsが載っていたりします。 例えば、Reactにはパフォーマンス最適化、TailwindCSSにはOptimizing for Productionというページが公式のドキュメントに記載されています。 各ライブラリのドキュメントをしっかり見てみましょう。 ライブラリに頼らず自前で作る ライブラリは万人向けに最適化されており、あなたのアプリケーション向けには最適化されていません。 あなたのアプリケーション以上に機能過多であることがほとんどです。 時には自前で作るのも1つの手です。 SPA編 ReactやVueといったコンポーネント志向のライブラリを想定しています。 Reactのコード例が多いですが、Vueでも参考になるかと思います。 コンポーネントがマウントされた後、遅延的にデータを読み込みする 優先順位だったりデータサイズが大きい場合等はマウント後リソースを取得します。 // 先にimportしない // import articles from './articles.json'; function ArticlesComponent() { const [articles, setArticles] = useState([]); // マウント後にデータを読み込む useEffect(() => { import('./articles.json').then(res => setArticles(res.default)); }, []) return articles.map(article => <div key={article.id}>{article.title}</div>) } コンポーネントを遅延読み込みする 初めてコンポーネントが表示されるタイミングでコンポーネントを読み込みます。 例えば、ユーザーがボタンをタップして初めて表示されるコンポーネントは遅延読み込みでの実装を考えます。 Reactで言えばSuspense、Next.jsならdyamicのAPIを使ってコンポーネントの遅延読み込みを実装できます。 SSRやSSG、ISRに移行する ReactやVueなど通常のSPAは性質上、初期描画が遅くなります。 ReactであればNext.jsやGatsuby.js、VueであればNuxt.jsといったフレームワークを使えば初期描画が遅くなる問題を解決できます。 コンポーネントの設計を最適化する ReactやVueだとコンポーネントのレンダリングの仕組みが違うので一概にこれが最適とは言えませんが、共通した設計の最適化があります。 例えば、「コンポーネントとデータの依存を考えて、再レンダリングの範囲を最小限にする」ことでしょう。 次のコンポーネントの例を見てください。 <!-- とあるコンポーネント --> <div> <div>データAに依存するUI部分</div> <div>データAに依存しないUI部分</div> </div> 1つのコンポーネント内に「データAに依存するUI部分」と「データAに依存しないUI部分」があります。 ReactであれVueであれこのようなケースの場合は「データAに依存しないUI部分」を別コンポーネントに切り出したほうが良いでしょう。 そうすればデータAに変更があった時、「データAに依存するUI部分」のみ再レンダリングさせることができます。 (Vueであれば問題ないですが、Reactの場合はステート管理のライブラリを使っていない場合はReact.memoを使う必要はあります) サーバー編 必要なデータのみフロントへ返却する 例えば、記事の一覧ページに各記事の本文を一部表示するとします。 「本文を一部」だけならサーバーからは一部だけ返却するようにします。 そうすることでファイルサイズ削減などができます。 事前に静的ファイルにしておく 都度APIへアクセスするのであれば予めJsonにしておくのも良いでしょう。 日本にあるサーバーを使う 日本向けのアプリを開発しているのであれば、地理的に近い日本のサーバーを選びましょう。 Brotli圧縮を使う gzipよりは圧縮後のサイズ削減や圧縮速度の向上が見込めます。 CDNを使う Amazon CloudFrontなどのCDNはできるなら使いましょう。 HTTP/2を使う できるなら使いましょう。HTTP/1.1より速いです。 HTTPキャッシュを使う Cache-ControlなどのHTTPヘッダーを利用して、ブラウザにリソースをキャッシュさせます。 まとめ この記事では次のようにカテゴリ分けしてWebフロントエンドのパフォーマンスチューニングのハウツーを紹介しました。 JavaScript編 HTML/CSSなどリソース編 ブラウザAPI編 V8エンジン編 ライブラリ編 SPA編 皆さんのパフォーマンスチューニング力の力添えになれば幸いです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webフロントエンドパフォーマンスチューニング55選

こんにちは、ぬこすけです。 近年、Webフロントエンドではサイトのパフォーマンスの重要性が高まっています。 例えば、GoogleはCore Web Vitalというパフォーマンスに指標を検索結果のランキング要因に組み込みました。 また、近年の某企業が「パフォーマンスの改善に取り組んだ結果、セッション数〇%アップ、CVR〇%アップ...」などの事例は枚挙にいとまがないでしょう。 パフォーマンスチューニングするためには、定量的に計測してボトルネックを探すようなトップダウンなアプローチもあります。 しかしながら、時には千本ノック的にハウツーを片っ端から試していくボトムアップなアプローチも有効になることもあったり、日々のコーディングでパフォーマンスを意識したコードを書くことは大切でしょう。 この記事ではパフォーマンス最適化のハウツーを紹介します。 パフォーマンス改善の施策が思い浮かばない時やフロントエンドのスキルを磨きたい時に辞書的な役割を果たせれば良いかなーと思っています。 ※私は55選書いている所で燃え尽きました。私自身も今後も更新する予定ですが、この記事は皆さんで作り上げたいと思っています。誤りや他にもこういうのあるよ!、この記事にわかりやすい説明あるよ!などあれば編集リクエスト、またはコメントいただけると幸いです。 ※この記事を読んでいる方にはこれからフロントエンジニアになりたい方、駆け出しエンジニアの方もいると思います。正直、何言ってるかわからない部分が結構あると思います。ですが、私の経験則上、「あの時書いてあったことはこういうことか!」と後々になって理解することがよくありました。今はよくわからないかもしれませんが、とりあえずストックなりしておいて、数ヶ月後にこの記事を見返すとまた理解度も変わるのかなーと思います。 注意事項 一口にフロントエンドといっても、SSRやらSSGやらでサーバー側も関わってくることもあるので、バックエンド寄りも話も混じっているので悪しからず。 わかりやすくするためにカテゴリに分けしていますが、微妙なカテゴリ分けのものもあるので悪しからず。 中には具体的なハウツーというより考え方みたいなものも混じっているかもしれませんが悪しからず。 環境によって必ずしもパフォーマンスが改善されるとは限らないので悪しからず。 あくまでパフォーマンスの観点なので他の観点では最適となるとは限らないので悪しからず。例えば、IndexedDBを紹介していますが、Sarafi 15で脆弱性が見つかっています。 紹介するものには特定のブラウザでしかサポートされていないものもあるので悪しからず。 JavaScript編 複数の非同期処理はPromise.allを使う もし互いに依存関係のない複数の非同期処理を実行しているのならば、Promise.allを使うのも手です。 async function notUsePromiseAll() { console.log('Start!!'); const response1 = await fetch("https://example.com/api/1"); const response2 = await fetch("https://example.com/api/2"); console.log('End!!'); } async function usePromiseAll() { console.log('Start!!'); const [response1, response2] = await Promise.all([ fetch("https://example.com/api/1"), fetch("https://example.com/api/2"), ]); console.log('End!!'); } Promise.allはいずれかの非同期処理が失敗すると全ての非同期処理が中断されます。 中断されたくない場合はPromise.allSettledが使えます。 非同期処理を待たなくて良い場合は待たない コードを眺めてみて、非同期処理を待たなくて良いところは待たないようにしましよう。 具体的には、もしasync/await構文を使っているならawaitを使わないことです。 const sendErrorToServer = async (message) => { // サーバーにエラー情報を送る処理 }; console.log('何かエラーが起きた'); // 後続の処理はサーバーにエラー情報を送る処理とは関係ないので await をつけない sendErrorToServer('エラーです'); console.log('後続の処理'); 先に非同期処理を走らせておく 互いに依存関係のある複数の非同期処理を実行する場合でも、時間がかかる処理の方を先に走らせておくのも良いでしょう。 const response1Promise = requestLongTime(); // ... // 色々処理 // ... const response1 = await response1Promise; const response2 = await requestShortTime(); console.log(response1, response2); キー/バリューを頻繁に追加や削除する場合はMapを使う MDNにも記載がありますが、キー/バリューのペアを頻繁に追加や削除する場合はObjectよりもMapを使ったほうが最適です。 const nameAgeMap = new Map() nameAgeMap.set('Tom', 19) nameAgeMap.set('Nancy', 32) nameAgeMap.delete('Tom') nameAgeMap.delete('Nancy') ... 膨大な配列の検索はキー/バリューで JavaScriptというよりかはロジックの問題かもしれません。 膨大な配列を検索する場合はキー/バリューに変換してから検索した方が速いです。 const thousandsPeople = [ { name: 'Tom', age: 19 }, { name: 'Nancy', age: 32 }, // ...めちゃくちゃ多い ] // 時間かかる const myFriend = thousandsPeople.find(({ name }) => name === 'Tom'); console.log(`The age is ${myFriend.age}`); const thousandsPeopleMap = { 'Tom': 19, 'Nancy': 32, // ... } // こっちのほうが速い const myFriend2 = thousandsPeopleMap['Tom']; console.log(`The age is ${myFriend2.age}`); 関数の結果をキャッシュする 頻繁に同じ引数で関数を実行したり、重い処理を走らせるなら関数の結果をキャッシュするのも有効です。 次のようなデコレータ関数を作れば、関数の結果をキャッシュできます。 function cachingDecorator(func) { const cache = new Map(); return x => { if (!x) { return func(x) } if (cache.has(x)) { return cache.get(x); } const result = func(x); cache.set(x, result); return result; } } function heavyFuncNoCache(str) { // 重い処理 } const heavyFunc = cachingDecorator(heavyFuncNoCache); heavyFunc('hoge'); // キャッシュから結果が返却される heavyFunc('hoge'); requireではなくimportを使う JavaScriptのモジュールの読み込み方にはrequireとimportの2種類があります。 requireは同期的、importは非同期的にモジュールを読み込むので、importの方が良いでしょう。 Node.jsといったサーバーサイドでJavaScriptを記述する場合はrequireを使うことが多いと思いますが、バージョン14であればpackage.jsonだったりファイルの拡張子をmjsにしたりいじることでimportで読み込めます。 なお、Qiitaのこの記事がわかりやすいです。 フェッチにはKeep-Aliveを指定する 何度も同じドメインへアクセスするのであればkeep-aliveを指定することでフェッチ処理が短縮されます。 import axios from 'axios'; import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; const httpAgent = new HttpAgent({ keepAlive: true }); const httpsAgent = new HttpsAgent({ keepAlive: true }); const keepAliveAxios = axios.create({ httpAgent, httpsAgent, }); keepAliveAxios.get(...); 非同期の関数を使う Node.jsには同期/非同期で別で用意されている関数があったりします。 例えばファイルに書き込みをする関数にはfs.writeFileSyncとfs.writeFileがあります。 もしフロントエンドアプリケーションのビルド時などに静的ファイルを生成する必要がある場合、特段理由がなければfs.writeFileを使いましょう。 HTML/CSSなどリソース編 imgやiframe、linkタグなどにimportance属性を追加する imgやiframe、linkタグなどではimportance属性を使うことでブラウザに読み込みの優先度を指定できます。 タグだけでなくfetch関数でもオプションでimportanceを指定できたりします。 imgやiframeタグにloading属性を追加する imgやiframeタグにはloading属性を使うことで読み込みのタイミングを指定できます。 もし、遅延/非同期読み込みしたい場合はloading='async'を使うと良いでしょう。 ただし、ファーストビューに使うと返って読み込みが遅くなる可能性もあるので注意しましょう。 imgタグにdecoding属性を追加する imgタグはdecoding属性を使うことでデコードを同期/非同期的に読み込むかを指定できます。 decoding='async'を指定すれば非同期的にデコード処理をブラウザに指示できます。 imgタグにはサイズを指定しておく imgタグのwidth/height属性などを使って、画像のサイズを指定しておきましょう。ブラウザのレンダリングの助けになります。 CLSの改善にも繋がります。 わからない場合は大体のサイズを指定しましょう。 優先度の高いリソースはlinkタグにpreloadを指定する ファーストビューに表示する画像など、優先度の高いリソースはlinkタグのrel属性にpreloadを指定ことで速い読み込みが期待できます。 優先度の高い外部ドメインへのアクセスがある時はlinkタグにdns-prefetchまたはpreconnectを指定する 外部ドメインからリソースを取得したり重要度の高い外部リンクを設置している場合などは、linkタグのdns-prefetchやpreconnectが使えます。 dns-prefetchはDNSルックアップ、preconnectは事前接続まで行います。 かなり優先度の高い外部ドメインへのアクセスはpreconnect、少し優先度が落ちる場合はdns-prefetchを使うと良いでしょう。 ユーザーがよく遷移するページはlinkタグにprerenderを指定する linkタグのrel属性にprerenderを指定することで、ブラウザは指定されたページをバック グラウンドでレンダリングします。 なので、ユーザーが指定されたページへ遷移する時はすぐに画面表示ができます。 ユースケースとしては、ランキングサイトのようなページで1位へのページへ遷移するユーザーは多いので、prerenderを指定しておくと良いかもしれません。 ただし、レンダリングされる都合上、ブラウザへの負荷が高かったり、JavaScriptで仕込んでいる計測処理が発火するなどの注意は必要です。 scriptタグにdeferやasync属性を追加する ブラウザでスクリプトが読み込まれるとHTMLやCSSの解析がブロックされます。 このような問題を解決するためにdeferやasync属性が使えます。 deferはHTMLやCSSの解析をブロックすることなくスクリプトを読み込んでおき、解析が完了したらスクリプトを実行します。 asyncはHTMLやCSSの解析とは独立してスクリプトの読み込み・実行をします。 Qiitaのこの記事がわかりやすいです。 優先度の高いリソースの読み込みはできるだけHTML上部で定義する ブラウザはHTMLドキュメントの上から解釈してきます。 なので、例えば同じpreloadを指定しているリソースでも、さらに優先度の高いものはよりHTML上部に定義して早めにブラウザが読み込めるようにしましょう。 CSSで余計なセレクタは書かない ブラウザはCSSセレクタを右から左に解析します。 なので、できる限り単一のクラス名やid名で指定した方が解析のスピードが上がります。 /* ブラウザは全てのdivタグを探し、さらに上の階層のhogeクラスを見つけようと解析する */ .hoge div {} /* Best Practice */ .hoge {} #hoge {} style属性を使って直接スタイルを指定する クラスなどセレクタを指定してCSSを書くよりも、直接HTMLタグのstyle属性を使ったほうがブラウザの解析は速いです。 ただし、コードの可読性やメンテが厳しくはなります。 <div style='color: red;'>ほげ</div> 不要なCSSを削除する 使っていないCSSは削除しましょう。 Chromeのデベロッパーツールを使えば不要なCSSを洗い出すことができます。 不要なJavaScriptを削除する 使っていないJavaScriptは削除しましょう。 例えば、console.logは基本的にプロダクションのコードでは不要なので、eslintで検出するなりbabelで削除するなりします。 ファーストビューに影響のあるCSSはheadタグの先頭で読み込む JavaScriptと違い、ブラウザのCSSの解析はHTMLの解析をブロックしません。 ファーストビューで読み込ませたいCSSはできるだけheadタグの先頭に読み込ませて、速くスタイリングされたファーストビューをユーザーに見せるようにしましょう。 ファーストビューに影響のないCSSはbodyタグの末尾で読み込む 逆にファーストビューに影響のないCSSはbodyタグの末尾で読み込ませることで、ブラウザにCSSの読み込みを遅延させます。 JavaScriptはbodyタグの末尾で読み込む ブラウザはJavaScriptの解析を始めるとHTMLやCSSの解析をストップします。 なので、JavaScriptはbodyタグの末尾で読み込み、HTMLやCSSの解析が終わった後のJavaScriptを解析するようにしましょう。 ただし、Google Analyticsなどの解析用のJavaScript等は除きます。 HTMLやCSS、JSをMinify/バンドルする Webpackやswcなどのバンドラーを使いましょう。 JavaScriptのトランスパイルを最新のESに合わせる もしJavaScriptをES2015でトランスパイルしている場合は、それよりも最新のバージョンでトランスパイルすることによって、JavaScriptのサイズを落とすことができます。 ただし、IEといった古いブラウザを切り捨てる覚悟は必要です。 画像はWebPやAVIFを使う 次世代の画像フォーマットとしてWebPやAVIFがあります。 こららの画像フォーマットを使うことで従来のPNG等の形式よりも画像サイズを縮小できたりします。 IKEAではAVIFによって画像の転送量を21.4%削減した例もあります。 画像サイズを縮小する 画質を落とすなり幅/高さを小さくするなりして画像サイズを縮小させます。 例えば、SVGでは作成したツールによってはコメントアウトが残っていたりで最適化されずに出力されている場合もあるので、手動で削除するなりツールを使うなりで縮小させます。 画像をインライン化する インライン画像としてHTMLに直接埋め込むことで、画像のリクエスト数を抑えることができます。 ただし、画像サイズが大きくなったりブラウザのキャッシュが効かない等のデメリットはあります。 画像サイズが小さく、一度しか読み込まれない場合などに有効といわれています。 過大なDOMを避ける DOMが多すぎるとブラウザの描画に負担をかけてしまいます。 不要なDOMを削除するのはもちろん、遅延読み込みや仮想無限スクロールなどを駆使してユーザーに表示されている部分だけ描画することで対策できます。 ブラウザAPI編 永続化ストレージはLocalStorageよりIndexedDBを使う ブラウザの永続化ストレージにはLocalStorageとIndexedDBが使えます。 LocalStorageは同期的、IndexedDBは非同期処理なので、IndexedDBの方がブラウザの動きを阻害することなくデータアクセスができます。 重たい処理やUIに依存しない処理はWebWorkerを使う WebWorkerを使うことでブラウザのメインスレッドとは別のスレッド立ち上げることができます。 フロントで検索機能といった重たい処理だったり、エラーをサーバーに送信するといったUIに依存しない処理はWebWorkerを使うことでメインスレッドの処理を阻害させません。 ServiceWorkerでリソースをキャッシュする ServiceWorker といえばPWA(Progressive Web Application)のイメージが強いですが、ブラウザから外部サーバーへのリクエストをフックしてHTMLやCSS、JSなどのリソースをキャッシュすることができます。 リクエストする際はキャッシュから取得することができるので外部サーバーへのリクエストするよりも処理が速くなります。 また、キャッシュから取得するか、先にサーバーへデータ取得してからキャッシュするかなど柔軟なキャッシュ戦略を選択できます。 ServiceWorkerを使う時はNavigationPreloadsも使う サイトにアクセス時、必要なリソースをフェッチする時にはServiceWorkerが起動するのを待ってフェッチ処理が走ります。 NavigationPreloadsではServiceWorkerの起動を待たずフェッチ処理を開始することができます。 WebAssembly を使う JavaScriptだけでなく、CやRustで書いたコードがブラウザで実行でき、JavaScriptよりも高速化される場合があります。 Amazonの事例もあります。 V8エンジン編 ChromeやNode.jsでは内部的にV8エンジンが使われています。 ここまで最適化すると変態ですが、チップスとして紹介します。 値の格納はコンストラクタで V8エンジンでは内部的にhidden classというものを生成します。 詳しい仕組みは割愛しますが、インスタンス化したオブジェクトに対して値を追加すると、新しいhidden classが生成されてしまいます。 class Point { constructor(x, y) { this.x = x; this.y = y; } } var p1 = new Point(11, 22); // hidden class の生成 var p2 = new Point(33, 44); // hidden class の再利用 p1.z = 55; // hidden class が生成されてしまう オブジェクトは同じ順番のプロパティで生成する これもhidden classに関わる話ですが、違う順番でプロパティを生成すると新たにhidden classが生成されます。 const obj = { a: 1 }; obj.b = 2 // hidden classを使い回せる const obj2 = { a: 1 }; obj2.b = 2 // 新しいhidden classが生成されてしまう const obj3 = { b: 2 }; obj3.a = 1 関数は同じ引数の型を使う 関数の引数はできるだけ同じ型を使うようにします。 function add(x,y) { return x + y } add(1,2) add(3,4) // OK add('3','4') // NG クラスはトップレベルのスコープで定義する 関数内でクラスを定義するのはV8エンジン的には良くないそうです。 // NG function createPoint(x, y) { class Point { constructor(x,y) { this.x = x this.y = y } } return new Point(x,y) } ライブラリ編 軽量なライブラリを採用する ライブラリを採用する1つの観点としてサイズがあります。 bundlephobia というサイトでライブラリのサイズをチェックすることができます。 ライブラリのサイズを減らす moment.js や lodash などのライブラリはWebpackのプラグインを使って不必要なスクリプトを削減することができます。 ライブラリのドキュメントを読む ライブラリの公式ドキュメントには最適化のTipsが載っていたりします。 例えば、Reactにはパフォーマンス最適化、TailwindCSSにはOptimizing for Productionというページが公式のドキュメントに記載されています。 各ライブラリのドキュメントをしっかり見てみましょう。 ライブラリに頼らず自前で作る ライブラリは万人向けに最適化されており、あなたのアプリケーション向けには最適化されていません。 あなたのアプリケーション以上に機能過多であることがほとんどです。 時には自前で作るのも1つの手です。 SPA編 ReactやVueといったコンポーネント志向のライブラリを想定しています。 Reactのコード例が多いですが、Vueでも参考になるかと思います。 コンポーネントがマウントされた後、遅延的にデータを読み込みする 優先順位だったりデータサイズが大きい場合等はマウント後リソースを取得します。 // 先にimportしない // import articles from './articles.json'; function ArticlesComponent() { const [articles, setArticles] = useState([]); // マウント後にデータを読み込む useEffect(() => { import('./articles.json').then(res => setArticles(res.default)); }, []) return articles.map(article => <div key={article.id}>{article.title}</div>) } コンポーネントを遅延読み込みする 初めてコンポーネントが表示されるタイミングでコンポーネントを読み込みます。 例えば、ユーザーがボタンをタップして初めて表示されるコンポーネントは遅延読み込みでの実装を考えます。 Reactで言えばSuspense、Next.jsならdyamicのAPIを使ってコンポーネントの遅延読み込みを実装できます。 SSRやSSG、ISRに移行する ReactやVueなど通常のSPAは性質上、初期描画が遅くなります。 ReactであればNext.jsやGatsuby.js、VueであればNuxt.jsといったフレームワークを使えば初期描画が遅くなる問題を解決できます。 コンポーネントの設計を最適化する ReactやVueだとコンポーネントのレンダリングの仕組みが違うので一概にこれが最適とは言えませんが、共通した設計の最適化があります。 例えば、「コンポーネントとデータの依存を考えて、再レンダリングの範囲を最小限にする」ことでしょう。 次のコンポーネントの例を見てください。 <!-- とあるコンポーネント --> <div> <div>データAに依存するUI部分</div> <div>データAに依存しないUI部分</div> </div> 1つのコンポーネント内に「データAに依存するUI部分」と「データAに依存しないUI部分」があります。 ReactであれVueであれこのようなケースの場合は「データAに依存しないUI部分」を別コンポーネントに切り出したほうが良いでしょう。 そうすればデータAに変更があった時、「データAに依存するUI部分」のみ再レンダリングさせることができます。 (Vueであれば問題ないですが、Reactの場合はステート管理のライブラリを使っていない場合はReact.memoを使う必要はあります) サーバー編 必要なデータのみフロントへ返却する 例えば、記事の一覧ページに各記事の本文を一部表示するとします。 「本文を一部」だけならサーバーからは一部だけ返却するようにします。 そうすることでファイルサイズ削減などができます。 事前に静的ファイルにしておく 都度APIへアクセスするのであれば予めJsonにしておくのも良いでしょう。 日本にあるサーバーを使う 日本向けのアプリを開発しているのであれば、地理的に近い日本のサーバーを選びましょう。 Brotli圧縮を使う gzipよりは圧縮後のサイズ削減や圧縮速度の向上が見込めます。 CDNを使う Amazon CloudFrontなどのCDNはできるなら使いましょう。 HTTP/2を使う できるなら使いましょう。HTTP/1.1より速いです。 HTTPキャッシュを使う Cache-ControlなどのHTTPヘッダーを利用して、ブラウザにリソースをキャッシュさせます。 まとめ この記事では次のようにカテゴリ分けしてWebフロントエンドのパフォーマンスチューニングのハウツーを紹介しました。 JavaScript編 HTML/CSSなどリソース編 ブラウザAPI編 V8エンジン編 ライブラリ編 SPA編 皆さんのパフォーマンスチューニング力の力添えになれば幸いです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【個人開発】Twitterで映える!ファンレターを交換・シェアして楽しめるサービス『ご縁箱』をリリースしました✧˖°。

はじめに... ▼ 皆さんはTwitterを使う上で このように感じたことはありませんか? 『 今ある ご縁を大切にしたい 』 『 フォロワーさんと感謝の気持ちを伝えたり、交流を深めたい! 』 『 あの人に話しかけてみたい! 』 けどきっかけがない..ちょっと億劫かも... こういった問題も楽しみながら解決出来る『 ご縁箱 』というサービスを開発しました。(Rails × Vue.js) ▼ ご縁箱を使うとどうなる? 実際に使ってみて... まだ話したことがなかったフォロワーさんと話すきっかけになった 色んなメッセージを頂いてほっこりした気分になった 自分の率直な気持ちをレターを送って伝えることで喜んでもらえた ご飯の約束ができた(全部焼き肉) 仲良しユーザーとの交流も深めることも出来ますが、まだ交流が出来ていないユーザーともコミュニケーションを取るきっかけにすることも出来るサービスです。 【今回作ったサービス】 https://goenbako.com 【私のご縁箱ページ】 https://goenbako.com/outputky スマホ・PC対応(※若干スマホ寄りのレイアウトです) サービスを作ったきっかけ 私は2021年の4月から10月初旬まで、オンラインのプログラミングスクール(RUNTEQ)で受講していました。 色んな方と交流を深めて刺激を受けたり、勉強会で新たな気付きを得た中で、 『そんな人達に普段は言わないようなことや感謝の気持ちを伝えたい!』 『一緒に楽しめて、その時の大事な気持ちを忘れないで残しておけるようなサービスを作ろう!!』 という想いでサービスを構想していきました。 ▼ 発想のタネ? 自分のmixiアカウントの紹介文を見たのがきっかけです。 mixiの紹介文は『 プロフィールに自分を紹介する書き込みをしてもらうことが出来る機能 』です。 ( いわゆる他己紹介 ) 現在、自分の周りは誰もINしていませんが紹介文一覧の不思議なアルバムのような空間がとても好きです。 人との繋がり、自身の感情の変化や当時から現在の軌跡が分かり、そこにとても価値を感じました。 Twitterの検索で 『mixi 紹介文』とワードサーチしたところ、同じように感じている方や、そういったサービスを求めている方が実際に多くいること知り、需要があることを確信しました。 この価値をTwitterというプラットフォームで表現・改良出来ないか?と考え、サービスのリリースに至っています。 サービス概要 ご縁箱はTwitter認証したユーザー同士でファンレターを交換・共有することが出来るSNS型のサービスであり、Twitter上で使えるコミュニケーションツールでもあります。 私は説明があまりお上手ではありません!!...百聞は一見に如かず。という言葉に頼って画像をよく使ってイメージをお伝えするという形を取らせていただきます。 認証しなくても一部の機能を楽しんでいただけますが、ここではTwitter認証してご縁箱を開設したことを前提とさせていただきます。 使い方 1. 自分のご縁箱ページをTwitterでシェア ツイート機能で共有するかプロフィールにリンクを載せることで、フォロワーさんに自分のページを共有します。 ▼ 例えば、これは私のご縁箱ページです。 https://goenbako.com/outputky Twitterでご縁箱ページを公開するか、このように公開している相手に対してレターを書くことでもSNSのようにご縁箱の中で繋がっていくことが出来ます。 2. レターを書く 交流したいユーザー、または応援している・気になるユーザーにレターを書くことが出来ます。 話題を自由に選ぶことができ、全て埋める必要もなく、入力した話題だけが送ったレターとして反映されます。 ただ漠然と 『 レターを書いてください! 』 と言われても書きづらくなる問題と 関係性問わず、誰でも負担少なく書けるように..という考えから、テーマを厳選しています。 ちなみに『ホーム』 ▶ 『送ったレター』から後で編集や削除も出来ます。 レターを書くのは楽しいです! 普段とは違った角度からその人のことを思い浮かべたり、その人の良い部分やポジティブな感情を届けるところがとても良いです。 つい真面目に書いてしまうのですが、適当にボケたりする風潮も好きです。 あなたのフォロワーさんがご縁箱を始めていたら是非、気軽に書いてみてください! 3. 受け取ったレターのシェア ( デザインOGP ) ※任意 受け取ったレターの項目を一つ選んでシェアすることが出来ます。 このタイミングでOGP画像を生成しているため遷移するまでに少しインターバルが発生します。 (具体的な処理内容については『 苦労した点 』で後述いたします) この間はユーザーに待たせることになるため、退屈に感じないように非同期処理で "移動しています..." というアナウンスを表示 & アニメーションを加えるようにしました。 受け取ったレターがデザインされたOGP画像となって反映されます。 Twitterでも気持ちの共有やリアクションをして話題を広げたり楽しむことが出来ます。 その他の機能紹介 レターの詳細 Twitter上でシェアされたツイートからアクセスすると ログインしていなくてもレターの全文を見たり、両者のユーザーページにも飛ぶことが出来ます。 シェアされたレターを通じてさらに第三者がファンレターを書くきっかけに少しでもなればいいなと思い、URL画像デザインとレター詳細ページのレイアウトも拘りました(^^) この一連の流れをユーザー同士で楽しみつつ、その過程で他のユーザーにも参加してもらい、ユーザーの縁が深まれば...というコンセプトです。 Twitterでフォロワーさんと交流を深めたい全ユーザーがご縁箱のターゲット層です!! ランダム訪問機能 ユーザーのページをワンタップでランダムにアクセスします。 新規のユーザーでも、ご縁箱の使われ方や世界観を感覚的に伝える目的と、 何か少し遊びを入れたいと思い実装しました。 単にユーザーのレコードをランダムに取得して画面遷移しているだけですが、意外と好評な機能です。 ユーザー検索機能 & 開設リクエスト機能 ユーザー検索機能はTwitterのIDにマッチするユーザーページに遷移します。 検索成功時の挙動(GIF画像) ▼ 検索失敗時は開設リクエストを案内 ユーザー検索でユーザーが存在しなかった場合、 『 見つかりませんでした。 』とただ表示されるだけだと、少し寂しくありませんか? (はい! 寂しいです!!) 検索したということはそのユーザーに対して興味やファンレターを書きたいという感情に近いものがあるはず.. その気持ちがなるべく無駄にならないように..なにか良い表現はないのか..?と考えた結果 検索ヒットしなかったユーザーにご縁箱をリプライで招待する機能を用意し、案内表示することにしました。 メール通知機能 ・ 設定ページ こちらはリリース後に要望/フィードバックを頂き、取り急ぎ実装いたしました。 RailsのActionMailerを使用しています。 実際に通知が来ると気持ち的に嬉しくなるので、実装して改めて良かったと思います(^^) 他にもプロフィール情報を更新したいという要望もいただき、リリース後に実装しました。 ひとまずは手動編集ではなく、twitter Gem を使って最新のクライアント情報を取得して更新するという仕様にしています。 実装について 使用技術 Ruby 2.7.4 Rails 6.0.4.1 JavaScript Vue.js 2.6.14 Gem sorcery carrierwave meta-tags twitter rubocop ライブラリ vuetify vue-router vuex vuex-persistedstate axios js-cookie vee-validate vue-gtag eslint その他 TwitterAPI heroku Amazon S3 ER図 細かめなこだわり ▼ 初心者ユーザーのみログイン時に、一度だけ使い方カードが表示される。 使い方がわかりづらいサービスのため、私のような感覚派かつ受動的なユーザーでも少しでも馴染みやすいように意識しました。 「受け取ったレターが0件の場合」という条件式と、Vuexで「1度表示したかどうか」のステートを保持させることで実現しています。 ▼ ログインしていなくてもレターを書くボタンを配置 ログインしていない場合はそもそもボタンを表示させないという実装が一番容易ではありましたが、導線を用意しておかないのは勿体ないと思い、ログインガイダンスを用意しました。 『登録する』『レターを書く』という明確な意志を持ったユーザーであれば別かもしれませんが、 ボタンすらない場合、興味本位で見に来てくれた新規ユーザーは本当に見るだけになってしまうのでは? 無意識でもファンレターを書くボタンを認識してもらうことには意味があると考え、このような設計にしました。 ハッシュタグを有効活用する 少しでもTwitterで認知していただきやすくするためにハッシュタグを一意でわかりやすいものにし、タグ一覧を見るだけでサービスの概要が掴めるようにしました。 Twitter単体で宣伝する際にも活用出来るものですので、最初にどう表現するかという部分まで慎重に決めました。 その他 全体的なレイアウト、デザイン(タイトルロゴ、ご縁箱イラストは例外) カラーやフォントで世界観を統一し、合わせて細かく素材を配置しました。 少しでもパフォーマンスの低下を抑えたかったため、Adobe Illustratorを使ってSVGのデザイン画像を自作しています。 デザインに関しては完全に素人でしたが、やってみたいと思い、試行錯誤を重ねつつ挑戦することにしました。 ログイン状態を保持しておく Twitterを行き来するサービスですのでvuex-persistedstateでログイン状態を保持するようにしました。 苦労した点 受け取ったレターをシェアする機能 デザインしたSVG画像をレターの内容をVueで反映させ、画像化した上でOGPに動的に反映させるという一連の処理の実装です。 この部分の実装です (Gif画像)画像のように文字を書き換えなくても画像はシェアはできますがツイート画面の状態で表示させるために書き換えています。 画像のように文字を書き換えなくても画像はシェアはできますがツイート画面の状態で表示させるために書き換えています。 SVG要素の扱い方、Canvas、フォントの適用やCSSの適用 OGP画像の仕組みやbase64画像をアップロードする方法 PC、スマホ、開発、本番 とそれぞれの環境下で所々動かないケース など、検証や問題の切り分けと根本的な知識の土台が必要となり、かなりの苦戦と時間を要しました。 具体的な処理の流れは以下のようになりました。 SVGの見えない部分の処理本番ではhiddenにしていますが2番までの処理は以下の画像のようになっています。 最初から表示してあるのが用意しておいたSVGのデザインフレームで、上に表示される画像がbase64画像化したものです。 画像のように文字を書き換えなくても画像はシェアはできますがツイート画面の状態で表示させるために書き換えています。 色々な記事を参考にしましたが、情報の掴み方が難しく、 一番の解決策はリファレンスなどを読み、土台となる知識を固めて挑むことでした。 初めて扱うオブジェクトや概念が多く、時間的な焦りや、厳しいと思うことが多々ありましたが、しっかり把握しながら実装すると着実に進めていけたため、とても楽しかったです。 終わりに 最後まで読んでいただき、ありがとうございました。 本記事もURL追記など改良していけたらと思いますので興味がある、良かったと思う方は是非ストックしていただいて、ご要望や感想などもコメントやTwitterで共有して頂けますと励みになります(^^) 良ければフォロワーさんに紹介していただいて、楽しんでください♪ https://goenbako.com
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む