20210613のJavaScriptに関する記事は28件です。

推しへの愛を叫べ!推し愛メーター

クラファン作成物への一歩 近々自作アイドルグッズをクラファンにかけようと思っています。 そのための第一歩として今回のものを作成しました。 クラファンは推しの声を自作グッズに閉じ込めるものを作成しようと思っています。 今回は声を数値化するものを作成しました。 推しへの愛ありますか? 皆さんの推しへの愛はどのくらいですか? その愛を声量に変えて可視化できるwebアプリケーションを作成しました。 ソースコード <div id="app"> <p> 推しを聞き取る※開発中<br> <button @click="listen">{{ recogButton }}</button> {{ result }} </p> </div> <div class="level-monitor"> <div class="indicator"></div> </div> <div class="percent"></div> <input type="button" value="推しへの愛を叫ぶ" onclick="main()"> :root { --opacity: 0.6; --radius: 0.2; --red-limit: 70%; --yellow-limit: 30%; } .level-monitor { width: 50px; height: 300px; background-color: pink; position: relative; border-radius: var(--radius); } .indicator { width: 80%; background-color: rgba(0, 0, 0, 0.3);*/ background-attachment: fixed; position: absolute; bottom: 0; left: 10%; border-radius: var(--radius); } .percent { width: 50px; font-family: Verdana, Geneva, Tahoma, sans-serif; font-size: 18px; text-align: center; padding: 5px 0; } const app = new Vue({ el: '#app', data: { recogButton: '聞き取り開始!', recog: null, result: '', speech: null, message: '', changecolor: 'red', }, mounted() { // 音声認識の準備 const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition || window.mozSpeechRecognition || window.msSpeechRecognition || null; this.recog = new SpeechRecognition(); this.recog.lang = 'ja-JP'; this.recog.interimResults = false; this.recog.continuous = false; // 音声認識が開始されたら this.recog.onstart = () => { this.result = ''; this.recogButton = 'ききとりちゅう…'; }; // 音声を認識できたら this.recog.onresult = (event) => { // 認識されしだい、this.resultにその文字をいれる // Vueなので、文字をいれただけで画面表示も更新される this.result = event.results[0][0].transcript; }; // 音声認識が終了したら this.recog.onspeechend = () => { this.recog.stop(); this.recogButton = '停止(クリックして再開)'; }; // 認識できなかったら this.recog.onerror = () => { this.result = '(認識できませんでした)'; this.recog.stop(); this.recogButton = '停止(クリックして再開)'; }; }, methods: { // 認識(聞き取り) listen() { this.recog.start(); }, // 認識(聞き取り) changecolor() { this.recog.start(); }, }, }); let maxvolume = 0; // Copied from https://stackoverflow.com/a/52952907/1204375 async function listen(cb) { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const audioContext = new AudioContext(); const analyser = audioContext.createAnalyser(); const microphone = audioContext.createMediaStreamSource(stream); const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1); analyser.smoothingTimeConstant = 0.8; analyser.fftSize = 1024; microphone.connect(analyser); analyser.connect(javascriptNode); javascriptNode.connect(audioContext.destination); javascriptNode.onaudioprocess = function () { const array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); let values = 0; const length = array.length; for (let i = 0; i < length; i++) { values += (array[i]); } const average = Math.round(values / length); cb(average); } } function updateBarIndicator(average) { const $el = document.querySelector('.indicator'); if(maxvolume<average){ maxvolume = average $el.style.height = `${maxvolume}%`; } } function updateTextIndicator(average) { const $el = document.querySelector('.percent'); $el.textContent = `${maxvolume}%`; } function main() { listen((percent) => { updateBarIndicator(percent); updateTextIndicator(percent); }); } 今回は以下のコードを参考に書きました。 レビュー 今回は母と妹にレビューをお願いしました。 前提として自分がなぜこれを作ったのかを説明しました。 フィードバック ・上のボタンは下のグラフには何も影響しないのか? ・機能がすくない ・Webアプリケーションと呼んでいいのか? かなり酷なフィードバックをいただきました。 上のボタンとグラフが連動しなかったのはソースコードを見ておかしいとおもったかたもいるかもしれませんがJavaScriptとVue.jsが混在しているんです。 サンプルを参照して作ったのですが変換がうまくいかず相互の関係がないものになってしまいました。 ほかの二つはもうぐうの音もでませんでした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

輪読メモ|JavaScript|applyとcallとbindのthisの対象についての確認

開眼! JavaScript――言語仕様から学ぶJavaScriptの本質を輪読会で読み進めている最中です。 本書の中で紹介されていたapplyやcallやbindについて、Chromeのデベロッパーコンソールを利用して復習・確認した?になります。 applyもcallもbindも第一引数にオブジェクトを指定することができます。 applyとcallは第二引数の形が異なり、bindはオブジェクトを指定するのみ(()で実行する)という浅い理解です。 今回確認したのは、オブジェクトの部分となります。 applyもcallもbindも実務で利用する経験がなく、あまり利用どころにピンと来ていないのが正直なところです。いずれも↑の輪読会をやっていく中でその存在を知りました。 事前準備 thisの違いを明確に分かりやすくするためにclassを実装する ついでにインスタンス化した変数(zzz)を用意しておきます。 class ZZZ { constructor(x,y) { this.x = x; this.y = y; } } const zzz = new ZZZ(1,2); // → ZZZ {x: 1, y: 2}x: 1y: 2__proto__: Object thisを出力するだけの関数を用意する ついでに呼び出してみます。 function KANSU(){ console.log(this) } KANSU(); // → Window {0: global, window: Window, self: Window, document: document, name: "", location: Location, …} thisを出力するだけの関数式を用意する ついでに呼び出してみます。 const KANSUSIKI = function(){ console.log(this); } KANSUSIKI(); // → Window {0: global, window: Window, self: Window, document: document, name: "", location: Location, …} thisを出力するだけのアロー関数を用意する ついでに呼び出してみます。 const ALLOW = () => { console.log(this); } ALLOW(); // → Window {0: global, window: Window, self: Window, document: document, name: "", location: Location, …} 違いを確認する 関数でapplyもcallもbindを確認してみる 引数として渡しているthisを参照していることが分かります。 KANSU.apply(zzz); // → ZZZ {x: 1, y: 2} KANSU.call(zzz); // → ZZZ {x: 1, y: 2} KANSU.bind(zzz); // ƒ KANSU(){ // console.log(this) // } KANSU.bind(zzz)(); // → ZZZ {x: 1, y: 2} 関数式でapplyもcallもbindを確認してみる 引数として渡しているthisを参照していることが分かります。 KANSUSIKI.apply(zzz); // → ZZZ {x: 1, y: 2} KANSUSIKI.call(zzz); // → ZZZ {x: 1, y: 2} KANSUSIKI.bind(zzz); // ƒ (){ // console.log(this) // } KANSUSIKI.bind(zzz)(); // → ZZZ {x: 1, y: 2} アロー関数でapplyもcallもbindを確認してみる 出力ログからwindowオブジェクトを参照していることが分かるため、引数として渡しているthisは利用されていないことが分かります。 ALLOW.apply(zzz) // → Window {0: global, window: Window, self: Window, document: document, name: "", location: Location, …} ALLOW.call(zzz) // → Window {0: global, window: Window, self: Window, document: document, name: "", location: Location, …} ALLOW.bind(zzz) // () => { // console.log(this); // } ALLOW.bind(zzz)() // → Window {0: global, window: Window, self: Window, document: document, name: "", location: Location, …} スクリーンショット/デベロッパーコンソール 事前準備 関数で試す 関数式で試す アロー関数で試す
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Denoで「自身のファイルからの相対パス」

相対パスでのファイル操作について~。 相対パスの解決方法 まず、Denoの相対パスの解決には2種類ある。 モジュール解決(import/export)の場合 自身のファイルからの相対パスが使用される。 Denoはインターネット上のスクリプトを直接実行できるが、その場合はリモートURLからの相対パスとして解決される。 例: https//xxx.com/mod.js //このコードが`https://xxx.com/mod.js`として実行またはインポートされている場合は、 import './bar' //=> `https://xxx.com/bar.js`がインポートされる それ以外の場合 Deno.readTextFile()などファイル操作の時は、カレントディレクトリからの相対パスが使用される。 カレントディレクトリは変動する可能性がある。 > cd /foo > deno eval "Deno.readTextFile('./bar.txt')" # /foo/bar.txt が読まれる > cd /foo/hoge > deno eval "Deno.readTextFile('./bar.txt')" # /foo/hoge/bar.txt が読まれる ファイルを操作する時に自身のファイルからの相対パスを使用したい場合は、次のようにする。 ファイル操作に「自身のファイルからの相対パス」を使用する方法 ここから本題。 URLオブジェクトとimport.meta.urlを使う。 import.meta.urlには自ファイルへのパスが入っているので、URLオブジェクトのコンストラクタを使って相対パスの解決を行う。 Denoのファイル操作系APIは、文字列だけでなくURLオブジェクトでもパスの指定が可能になっているため、URLオブジェクトをそのまま渡せばよい。 const path = new URL("./foo.txt", import.meta.url) // ./foo.txtが絶対パスに変換される Deno.readTextFile(path) なお、インターネット上にこのファイルを置いた場合はURLオブジェクトがリモートファイルを指してしまい、機能しないので注意。 (これをリモートとローカルの両方で動作させるための機能として、ローカルファイルのfetchが検討されているようだ。) あとdeno compileしたときの挙動も変わってきそうではある。(未調査)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue-realworld-example-appを読んでみた

(giithub): https://github.com/gothinkster/vue-realworld-example-app (CodeSandbox): https://codesandbox.io/s/github/gothinkster/vue-realworld-example-app/tree/master ?はじめに ? 記事の背景 vue-realworld-example-app は、実際の使用例(CRUD、認証、高度なパターンなど)に近い形のVue.jsプロジェクトである。Webアプリケーションの基本的な機能が一通りまとまっている。 本記事は、一般的なVue.jsプロジェクトはどのような構成になっているか、どのような技術が使われているかを知るために、vue-realworld-example-app をコードリーディングし、まとめたものである。 ? ( 参考 前提知識 ) コードリーディングを行うにあたる前提知識は以下。 Vue.js v2 (Router、Vuex)の基礎知識 Vue CLI の基礎知識 また、知っているといいと感じたものは以下。(知識がなくても調べながらでOK) REST API の基礎知識 JWTの基礎知識 ローカルストレージの基礎知識 ?全体構成の確認 ?フォルダ構成 /src構成は以下の通り。 App.vue、 main.js アプリのエントリーポイント。アプリ全体の設定などはここで行う。 /views 表示する画面。ルーティングと対応している。 /router Vue Routerによるルーティングの設定を行っている。 /components アプリ内で使用するコンポーネント。 /store Vuexによる状態管理。/views, /components内のから呼ばれる。 /common API、JWT、フィルタ、設定などのアプリ全体で使用する共通機能。 ?コンポーネント構成 コンポーネント構成は下記の通り。「緑=/views、黄色=/components」である。/ruterでルーティングされているコンポーネントは/viewsに、それ以外は/componentsで管理している。 ? /store, /commonの構成 /store, /commonの関係性は下記のとおり。 /storeは、4つのモジュールをexport している。それぞれのモジュールは、state, getters, actions, mutationsを持ち、状態管理を行う。状態管理を行う中で、インターフェースのような役割を果たすtypeから関数名を参照したり、共通処理を/commonを参照し、実行している。 /commonは、/storeで使用する共通処理ファイル(.service.js)とその設定ファイル(config.js)、アプリ全体で使用するフィルターファイル(.filter.js)を持つ。 ? ソースコード詳細 各ソースコードの気になったところを抜粋して記載する。 ソースコード: https://github.com/gothinkster/vue-realworld-example-app ? main.js、App.vue ? main.js import DateFilter from "./common/date.filter"; import ErrorFilter from "./common/error.filter"; Vue.filter("date", DateFilter); Vue.filter("error", ErrorFilter); アプリ全体で使用する、日付変換(DateFilter)や エラー変換(ErrorFilter)のフィルターはここで定義する。これらは/commonで定義されている。 import ApiService from "./common/api.service"; ApiService.init(); http request のための初期化を行う。 ApiService.init()では、vue-axiosプラグインを適用し、デフォルトURLを設定している。 import { CHECK_AUTH } from "./store/actions.type"; router.beforeEach((to, from, next) => Promise.all([store.dispatch(CHECK_AUTH)]).then(next) ); router.beforeEach()はページロードごとに呼ばれる関数。Promise.all()は、配列を引数にとり、すべてのPromiseを実行する。 ここでページロードごとにトークンの認証処理をしている。 参考 Vue公式 ナビゲーションガード 参考 MDN Promise.all() ? App.vue <template> <div id="app"> <RwvHeader /> <router-view /> <RwvFooter /> </div> </template> アプリ全体に表示させるため、ヘッダおよびフッタはここに記載する。 ? / router ? index.js routes: [ { path: "/", component: () => import("@/views/Home"), children: [ { path: "", name: "home", component: () => import("@/views/HomeGlobal") }, ... ], ... }, ... ] childrenを使用することで、ネストされたルートにしている。 またnameをつけることで、<router-link :to={ name: 'home' }>と指定できる。 ? /views ? / views / Home.vue 周辺 Home.vue <router-link>で HomeGlobal、HomeMyFeed、HomeTag にリンクを設定し、 <router-view>で表示する仕組み。 <script> import { mapGetters } from "vuex"; import { FETCH_TAGS } from "@/store/actions.type"; export default { // ... mounted() { this.$store.dispatch(FETCH_TAGS); }, computed: { ...mapGetters(["isAuthenticated", "tags"]), tag() { return this.$route.params.tag; } } }; </script> mounted()はVuexのactionを使用してFETCH_TAGSを実行し、APIを用いてtagsを取得する。取得したtagsは、Vuexのstateで管理されるので、mapGetter()で取得する。という流れ。 mapGetters()は引数で指定したstateの値を取得する関数。import {mapGetters} from "vue"を使用してインポートし、computedで使用する。 HomeGlobal.vue, HomeMyFeed.vue, HomeTag.vue <template> <div class="home-global"><RwvArticleList type="all" /></div> <!-- <div class="home-my-feed"><RwvArticleList type="feed" /></div> <div class="home-tag"><RwvArticleList :tag="tag"></RwvArticleList></div> --> </template> 上記3つのtemplateの中身はどれも似た構成。@/components/ArticleListを呼び、属性 ( type )を変えているだけ。 ?/ views / Login.vue , Register.vue, Settings.vue Login.vue <form @submit.prevent="onSubmit(email, password)"> ... </form> v-modelでバインディングして、ボタン押下すると値が送信されるごくごく一般的なフォームの実装となっている。@submit.preventはevent.preventDefault()を呼び出す処理。これにより、フォーム送信後もページのリロードを行わない。.preventはイベント修飾子と呼ばれる。 import { mapState } from "vuex"; import { LOGIN } from "@/store/actions.type"; export default { ... data() { return { email: null, password: null }; }, methods: { onSubmit(email, password) { this.$store .dispatch(LOGIN, { email, password }) .then(() => this.$router.push({ name: "home" })); } }, ... }; ログイン成功したら、.then(() => { this.$router.push({ name: "home" })});を行い、ホームに飛ばす処理になっている。 import { mapState } from "vuex"; export default { ... computed: { // auth.errors を errors に代入する処理。 // ↓ 加工後の名称: state => 加工処理(state.加工対象のステート名) ...mapState({ errors: state => state.auth.errors }) } }; 上記の書き方は、stateを加工した結果をバインドする方法。 Register.vue Login.vueとほぼ同じ構成。違いは、フォームにusernameの欄ができて、Vuexのactionが、LOGINからREGISTERになったくらい。 Setting.vue Login.vueやRegister.vueとほぼ同じ構成。 ?/ views / Profile.vue 周辺 Profile.vue <router-link>で ProfileArticles、ProfileFavorited にリンクをはり、<router-view>で表示する仕組み。 <div v-if="isCurrentUser()"> <router-link class="btn btn-sm btn-outline-secondary action-btn" :to="{ name: 'settings' }" > ... </router-link> </div> <div v-else> <button class="btn btn-sm btn-secondary action-btn" v-if="profile.following" @click.prevent="unfollow()" > ... </button> <button class="btn btn-sm btn-outline-secondary action-btn" v-if="!profile.following" @click.prevent="follow()" > ... </button> </div> <div v-if="isCurrentUser()">でログイン中のユーザかどうかで表示画面が変わる。 v-if="profile.following"部分も同様で、フォローしているかどうかで表示画面が変わる。 import { FETCH_PROFILE, FETCH_PROFILE_FOLLOW, FETCH_PROFILE_UNFOLLOW } from "@/store/actions.type"; export default { ... mounted() { this.$store.dispatch(FETCH_PROFILE, this.$route.params); }, ... watch: { $route(to) { this.$store.dispatch(FETCH_PROFILE, to.params); } } }; 初期化時にmounted()で、ページのユーザ名をパラメータから取得している。 Vue.jsではパラメータが #/@hoge から #/@huga へ遷移するときに同じコンポーネントインスタンスが再利用されるのでwatch: $route(to){...}を使用して、パラメータの検知をおこなっている。参考 ProfileArticles.vue、ProfileFavorited.vue <template> <div class="profile-page"> <RwvArticleList :author="author" :items-per-page="5"></RwvArticleList> <!-- <RwvArticleList :favorited="favorited" :items-per-page="5"> </RwvArticleList> --> </div> </template> templateのはどれも似た構成。@/components/ArticleListを呼び、属性 ( author, favorited )を変えているだけ。 ?/ views / Article.vue import store from "@/store"; import { FETCH_ARTICLE, FETCH_COMMENTS } from "@/store/actions.type"; export default { //... beforeRouteEnter(to, from, next) { Promise.all([ store.dispatch(FETCH_ARTICLE, to.params.slug), store.dispatch(FETCH_COMMENTS, to.params.slug) ]).then(() => { next(); }); } //... } beforeRouteEnter()を用いて、FETCH_ARTICLE、FETCH_COMMENTSを呼び、articleとcommentの最新のものを取得しstateを更新する。 <div v-html="parseMarkdown(article.body)"></div> import marked from "marked"; // ... export default { // ... methods: { parseMarkdown(content) { return marked(content); } } } markedはマークダウン解析ツール、htmlを返すため、v-htmlディレクティブを使用している。 ?/ views / ArticleEdit.vue // [/editor/:slug] => [/editor] の場合、エディタを空にして表示する。 // beforeRouteUpdateは、パラメータが変わったタイミングでも実行される。 async beforeRouteUpdate(to, from, next) { await store.dispatch(ARTICLE_RESET_STATE); return next(); }, // [/editor] の場合、下記のifがfalseになり、実行されない。 // [/editor/:slug] の場合、下記のifがtrueになり、実行される。 async beforeRouteEnter(to, from, next) { await store.dispatch(ARTICLE_RESET_STATE); if (to.params.slug !== undefined) { await store.dispatch( FETCH_ARTICLE, to.params.slug, to.params.previousArticle ); } return next(); }, // [/editor/:slug] から去るときエディタを空にする。 async beforeRouteLeave(to, from, next) { await store.dispatch(ARTICLE_RESET_STATE); next(); }, ルートが変更したら実行されるナビゲーションガードで制御する。ARTICLE_RESET_STATEで記事エディタを空に更新するが、/editor/slugのような場合は、元記事が記載されたまま表示する。 ※ ちなみに上記のナビゲーションガードは、パラメータが変わったタイミングでは実行されないので、 /editor から/editor/:slugに遷移した場合、元記事が記載されない。 参考:https://tsudoi.org/weblog/5738/ 参考:https://router.vuejs.org/ja/guide/advanced/navigation-guards.html <template> ... <input type="text" class="form-control" placeholder="Enter tags" v-model="tagInput" @keypress.enter.prevent="addTag(tagInput)" /> <div class="tag-list"> <span class="tag-default tag-pill" v-for="(tag, index) of article.tagList" :key="tag + index" > <i class="ion-close-round" @click="removeTag(tag)"> </i> {{ tag }} </span> </div> ... </template> タグの登録処理は、@keypress.enter.prevent="addTag(tagInput)"のように、Enter押下時にタグ登録処理を行う実装となっている。 ? /components ヘッダを表示するコンポーネント。 ?/ components / TheHeader.vue <ul v-if="!isAuthenticated" class="nav navbar-nav pull-xs-right"> .... </ul> <ul v-else class="nav navbar-nav pull-xs-right"> .... </ul> 認証状態によって表示する画面をv-ifで切り替えている。 <router-link class="nav-link" active-class="active" exact :to="{ name: 'profile', params: { username: currentUser.username } }" > {{ currentUser.username }} </router-link> <router-link>では:to="{params:{...}}でパラメータを指定することができる。 ? / components / VTag.vue タグを表示するコンポーネント。 <router-link :to="homeRoute" :class="className" v-text="name"></router-link> v-text="name"は{{ name }}と同じ export default { props: { name: { type: String, required: true }, className: { type: String, default: "tag-pill tag-default" } } // ... } propsでは、型の指定(type)、必須項目指定(required)、デフォルト値(default)などプロパティの型を指定できる。以降で説明するコンポーネントのpropsにおいても、型の指定などを必ず行っていた。 ? / components / ArticleMeta.vue 周辺 ArticleMeta.vue 記事作成者の情報などで構成されるコンポーネント。 <span class="date">{{ article.createdAt | date }}</span> main.jsで定義したフィルターが使用されている。 <rwv-article-actions v-if="actions" :article="article" :canModify="isCurrentUser()" ></rwv-article-actions> フォロー、いいねボタンはArticleActionsコンポーネントで制御している。 ArtcleAction.vue フォローやいいねボタンを構成するコンポーネント <span v-if="canModify"> <!-- Edit Article, DeleteArticleボタンが表示 --> </span> <span v-else> <!-- Follow, Favoriteボタンが表示 --> </span> ユーザが記事の作成者か否かで表示画面を変えている。 また、classを変えるなどの見た目の変更はcomputed、 ボタン押下時の処理はmethodsで行う。 ? / components / CommentEditor.vue 周辺 CommentEditor.vue 記事に対するコメントフォームを構成するコンポーネント。 <RwvListErrors :errors="errors" /> <form class="card comment-form" @submit.prevent="onSubmit(slug, comment)"> methods: { onSubmit(slug, comment) { this.$store .dispatch(COMMENT_CREATE, { slug, comment }) .then(() => { this.comment = null; this.errors = {}; }) .catch(({ response }) => { this.errors = response.data.errors; }); } } フォームのPostボタンを押下すると、COMMENT_CREATEが実行される。この処理が失敗すると、RwvListErrors部分でエラーメッセージが表示される。 ListError.vue errorsオブジェクトを受け取った時に、エラーメッセージを画面に表示するコンポーネント。 ? / components / Comment.vue コメント表示のコンポーネント。 <span v-if="isCurrentUser" class="mod-options"> <i class="ion-trash-a" @click="destroy(slug, comment.id)"></i> </span> computed: { isCurrentUser() { if (this.currentUser.username && this.comment.author.username) { return this.comment.author.username === this.currentUser.username; } return false; }, // ... } @click="destroy(slug, comment.id)"はクリックイベントでコメントを削除する。isCurretntUserが現在のユーザと著者が同じ場合はこのボタンを表示する。 ? / components / ListErrors.vue errorsオブジェクトを受け取った時に、エラーメッセージを画面に表示させるコンポーネント。CommentEditor.vueでも使用している。 ? / components / ArticleList.vue 記事プレビューの一覧を表示するコンポーネント。 大まかな処理は、listConfigに記事情報を持たせて、currentpage、 type、 author、 tag、 favoritedをwatchで監視し、変更があったらfetchArticle()で新しい記事を取得する流れである。 ? / components / VArticlePreview.vue 周辺 VArticlePreview.vue 記事プレビューを表示するコンポーネント。 ArticleMeta.vueとTagList.vueコンポーネントを使用している。 TagList.vue タグ一覧を表示するコンポーネント。 ? / components / VPagination.vue ページ割りの表示をするコンポーネント。 <li v-for="page in pages" :data-test="`page-link-${page}`" :key="page" :class="paginationClass(page)" @click.prevent="changePage(page)" > <a class="page-link" href v-text="page" /> </li> :class="paginationClass(page)"で現在のページと一致していたらactive-classをつけ、表示デザインを変える。 ページ割りのボタンが押下されたら、@click.prevent="changePage(page)"を実行する。 changePage(goToPage) { if (goToPage === this.currentPage) return; this.$emit("update:currentPage", goToPage); }, 親コンポーネント(ArticleList.vue)では、以下のようにコンポーネントを使用している。 <VPagination :pages="pages" :currentPage.sync="currentPage" /> .sync修飾子をつけることで、子コンポーネントからthis.$emit('update:currentPage', goToPage) で親に通知することができる。(親は update:prop名のイベントを監視) 参照1【Vue】知っておきたい .sync修飾子のすゝめ、参考2_公式 ? /store ? / store / index.js import Vue from "vue"; import Vuex from "vuex"; import home from "./home.module"; import auth from "./auth.module"; import article from "./article.module"; import profile from "./profile.module"; Vue.use(Vuex); export default new Vuex.Store({ modules: { home, auth, article, profile } }); ストアオブジェクトを複数のファイルで管理するために、modulesを使って分割を行う。 ? / store / .modules.js .modules.jsは共通して以下の4つをexportしている。参考:Vuex公式 export default { state,   // 状態管理利を行う対象 actions, // 非同期処理を行う。mutationsを呼ぶ。 mutations, // stateの状態を変更する getters // stateを取得する }; auth.modules.js import ApiService from "@/common/api.service"; import JwtService from "@/common/jwt.service"; import { LOGIN, LOGOUT, REGISTER, CHECK_AUTH, UPDATE_USER } from "./actions.type"; import { SET_AUTH, PURGE_AUTH, SET_ERROR } from "./mutations.type"; APIおよびJWTの処理は共通処理として切り出している。また、actionとmutationで使用する関数は、.typeとして別ファイルで定義している。 const state = { errors: null, user: {}, isAuthenticated: !!JwtService.getToken() }; isAuthenticated: !!JwtService.getToken()の!!は二重否定を行い、Boolean型にキャストする処理。 const getters = { currentUser(state) { return state.user; }, isAuthenticated(state) { return state.isAuthenticated; } }; ユーザ状態と認証状態を返す。 const actions = { [LOGIN](context, credentials) { return new Promise(resolve => { ApiService.post("users/login", { user: credentials }) .then(({ data }) => { context.commit(SET_AUTH, data.user); resolve(data); }) .catch(({ response }) => { context.commit(SET_ERROR, response.data.errors); }); }); }, [LOGOUT](context) { context.commit(PURGE_AUTH); }, // ... } const mutations = { [SET_ERROR](state, error) { state.errors = error; }, [SET_AUTH](state, user) { state.isAuthenticated = true; state.user = user; state.errors = {}; JwtService.saveToken(state.user.token); }, [PURGE_AUTH](state) { state.isAuthenticated = false; state.user = {}; state.errors = {}; JwtService.destroyToken(); } }; APIを呼び、成功したらSET_AUTH、失敗したらSET_ERROR、ログアウト時はPURGE_AUTHと、mutationにコミットする。 ? /common ? / common / .service.js api.service.js import Vue from "vue"; import axios from "axios"; import VueAxios from "vue-axios"; import JwtService from "@/common/jwt.service"; import { API_URL } from "@/common/config"; axios、vue-axiosを使用している。参考 vue-axios トークンの処理、ベースURLの管理は別ファイルに分離している。 const ApiService = { init() {/**/}, setHeader() {/**/}, // http method query(resource, params) {/**/}, get(resource, slug = "") {/**/}, post(resource, params) {/**/}, update(resource, slug, params) {/**/}, put(resource, params) {/**/}, delete(resource) {/**/} }; export default ApiService; export const TagsService = { get() { return ApiService.get("tags"); } }; export const ArticlesService = { // ... }; export const CommentsService = { // ... }; export const FavoriteService = { // ... }; ファイルの中でも処理の分割を行っている。ApiServiceで初期化、ヘッダの設定、http requestの処理がまとまっていて、それらを用いてTagService、ArticlesServiceなど、具体的な処理を実装していることがわかる。 jwt.service.js const ID_TOKEN_KEY = "id_token"; export const getToken = () => { return window.localStorage.getItem(ID_TOKEN_KEY); }; export const saveToken = token => { window.localStorage.setItem(ID_TOKEN_KEY, token); }; export const destroyToken = () => { window.localStorage.removeItem(ID_TOKEN_KEY); }; export default { getToken, saveToken, destroyToken }; ローカルストレージでトークンを管理している。 ※ ローカルストレージでトークンを管理するのはセキュリティ上よくないと聞いたことがあるがどうなのだろうか?要調査 参考 MDN localStrage ? / common / .filter.js main.jsにてVue.filter(xxx)で使用している。 関数で定義され、フィルターをかますと、戻り値の値に変換される。 参考 : https://jp.vuejs.org/v2/guide/filters.html 所感 Vue.jsに触れてこのリポジトリを見つけたときからいつかは理解したいと思っていたので、一通り理解できた???のでよかったです。 記事を見直すと、トップダウンで書いてきたので、後から出てくる機能が前で使用されていてわかりにくいと感じました。記載の工夫が必要と感じました。ここまで書いたので、これからも定期的に読み直して、不足分は追記していこうと思います。 コードリーディングした感想は、 プログラミングと同じくらい、構成の設計は大切 特に、処理は小さく分割する、機能をまとめることが重要 リーディングを行う際、全体のファイルの関係性を整理すると、頭に入りやすい リポジトリを読むときは、Issuesを見るとヒントが隠れている 実はこれはVue2のプロジェクトで、数年間更新がされていないリポジトリです。今後は、学んだことを自分のアプリに適用すること。それと並行して、Vue3のReal World example appのリーディングに取り組んでいきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nullやundefinedなどの判定でちょっと迷った話

変数でnullやundefined(、空文字、0)を宣言し、判別するときに少しだけ戸惑うことがあった。 今回はJavascriptで書く。 const a = null if (!a) { // undefined または null のときの処理 } このように書くと、nullにもかかわらず変数aが宣言されているので、上記の判定式がfalseと勘違いしてしまう事がある。 自分が間違えやすい原因 今まで自分は空やundefinedではない変数しか宣言しなかったためにこのような戸惑いが起きたと考えられる。 しかし、下記のように書けばすぐ理解できる。 const a = null if (a == null) { // undefined または null のときの処理(このように書けば一瞬で理解できる) } nullやundifindなどを理解している、プログラミングに慣れている方々は当然のように思うかも知れないが、自分のような初心者は一瞬でも迷うことがあるので、これを機に間違えないようにしたい。 参考 https://www.youtube.com/watch?v=KQhyHHQrcic https://qiita.com/tadnakam/items/ffaef84ee89fe659ed15
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon Cognitoで認証したGoogleユーザーのアクセストークンを使ってLamdaからGoogle APIを叩く

はじめに Cognitoで認証したユーザーでLamdaからGoogle APIを叩く方法を備忘録として書きます。LambdaからGoogle APIを叩いて、スプレッドシートに書き込みます。 * CognitoとApiGatewayやAppsyncの連携についてはこの記事で扱わないので、他の記事を参考にしてください。 環境 Lambda (Nodejs) AppSync appsyncからLambdaを呼び出していますが、API Gatewayから呼び出した場合でも、同様の実装で実行できると思います。 Cognitoの設定 以下のページを参考に、GoogleのApp IDとシークレットを取得します。 取得したIDとシークレットをCognitoのIDプロバイダーに登録して、Scopeを設定します。 Google APIのScopeは以下のページを参考にしてください。今回は、「openid」、「email」、「profile」とスプレッドシートの操作に必要な「https://www.googleapis.com/auth/spreadsheets 」を入力します。 https://developers.google.com/identity/protocols/oauth2/openid-connect#scope-param https://developers.google.com/identity/protocols/oauth2/scopes アプリクライアントの「有効なIDプロバイダー」にGoogleが追加されていると思うので、有効にします。 次に、アクセストークンを取得できるようにカスタム属性を設定します。「変更可能」にチェックが入っていないとアクセストークンが新しくなったときに上書きできなくなるので、必ずチェックを入れるようにしてください。 画像ではカスタム属性が2個ありますが、赤枠で囲んだほうだけを作成してください。 そして、アクセストークンがマッピングされるように属性マッピングの設定を追加します。これで、custom:google-accessTokenでアクセストークンが取得できるようになります。 上記の手順でGoogleを追加するとクライアント側では以下のようにGoogleでログインができるようになると思います。 LambdaでAcess Tokenを使う Cognitoで認証したユーザーからLambda関数が実行されたとき、eventにカスタム属性が入っています。今だとevent.identity.claims["custom:google-accessToken"]にアクセストークンが追加されているので、それを取り出して使用します。 取り出したアクセストークンをgoogleapisに設定してGoogle のAPIを呼び出すことができます。 ちなみに、これはGraphQLのMutationが呼び出されたときに実行される関数で、入力された値を変換してスプレッドシートに書き込んでいます。 import { AppSyncIdentityCognito, AppSyncResolverHandler } from "aws-lambda"; import { google } from "googleapis"; import { CreateMovieInput, MutationCreateMovieArgs } from "./generated/graphql"; const oauth2Client = new google.auth.OAuth2( "xxxxxxxxxxxxxxx(Client ID)", "xxxxxxxxxxxxxxx(Client Secret)" ); export const handler: AppSyncResolverHandler<MutationCreateMovieArgs, string> = async (event) => { const identity = event.identity as AppSyncIdentityCognito; const access_token = identity.claims["custom:google-accessToken"]; const tokens = { access_token, // refresh_token: "", }; oauth2Client.setCredentials(tokens); const { input } = event.arguments as MutationCreateMovieArgs; // inputToValuesは入力をスプレッドシートシートに書き込む値に変換する独自関数 const values = inputToValues(input); const sheets = google.sheets({ version: "v4", auth: oauth2Client }); // 指定したスプレッドシートの最後尾にデータを追加する const data = await sheets.spreadsheets.values.append({ spreadsheetId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx", valueInputOption: "USER_ENTERED", insertDataOption: "INSERT_ROWS", range: "A1", requestBody: { values, }, }); return data.statusText; }; マッピングの設定でrefresh_tokenもマッピングするようにすれば、リフレッシュトークンも使えるようになります。 まとめ Amazon Cognitoで認証したGoogleユーザーのアクセストークンを使ってLamdaからGoogle APIを叩く方法について紹介しました。 設定は簡単にできるのでぜひ試してみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ガンプラで遊ぶためにAR.jsでコックピット視点カメラを作った

30代男子。ガンプラで遊んでます! 最近、自分の部屋ができたこともあり、久しぶりにガンプラを作っています。 作ったガンプラは部屋に飾ったり写真を撮ったりしているのですが、ガンプラ自体のクオリティが昔より上がっていたり、スマホのカメラ自体もすごく高性能になっていることで、簡単にカッコイイ写真が撮れるのでテンション上がります。 (バンダイ様のおかげです。私のようなパチ組専門でもすごくカッコイイです。) 実は今度クラファンにブンドド※用のカメラデバイスを出そうとしているのですが、最近AR.jsなるもので手軽にWebでARアプリができることを知ったので、これを使って超プロトタイプ的にブンドドを更に楽しくするARアプリを作ってみました。 ※ブンドドっていうのはプラモデルやフィギュアを使ってやる所謂「ごっこ遊び」的なものです。 コックピット視点でブンドドしたいなーということで 作ってみました。 仕組みは単純でカメラ映像にAR.jsでコックピットの3Dモデルを重ねただけです。 ↓ここから遊べます。 先日WEBアプリでもARできるって知ったんで、コックピット視点でガンプラ眺めるアプリ作ってみた。(スクショも撮れるよ。)コックピット作りこんだり、カメラをリモートにしたりできると面白くなるかな。#ガンプラ#拡張現実 pic.twitter.com/BCEgMROtDc— shoito66 (@shoito66) June 12, 2021 動画はスマホで撮ったもので、PCで見るとこんな感じ。 (Webカメラのピントが上手く合わず・・・) 仕組みと作り方 今回のアプリは冒頭でも触れている通り、AR.jsというライブラリを活用したWebアプリケーションになっています。 PCやスマホのカメラで移した映像にAR.jsでコックピットの3Dモデルを重ねて表示しています。 ↓イメージ的にはこんな感じ 3Dモデルの準備 コックピットの3Dモデルはこちらのサイトで無料のものをダウンロードして使用しました。 少しブレンダーで弄りました。 ブレンダーで書き出したモデルをAR.jsで使用する方法は以下の記事にまとめてあります。 Webアプリケーションの作成 今回作成したアプリケーションのコードはこちらです。 See the Pen コックピット視点を楽しもう! by shoito66 (@shoito66) on CodePen. スタイルやスクリプトにも色々書いてありますが、これはスクリーンショットを取ったり保存したりする部分ですので、ARについてはHTMLだけで完結しています。 スクリーンショットを取る部分はこちらの記事を参考にさせていただきました。 周りの人に使ってもらってみた 今回はクラファンに向けたプロトタイプという意味も強いので、折角ならと身近な人に使ってもらいました。 使ってもらった相手は私の奥さん。 頼む前は勝手に以下の様な想像をしていました。 ・ガンダムはあまり知らないからこれを使ってブンドドの世界が楽しくなるかはあまり気にしなさそう ・エンジニアなので、起動時間やスクショ部分の使い勝手等についての意見ならもらえるかな とか思っていたら、実際に使ってもらって返ってきたフィードバックは以下のようなものでした。 曰く「コックピットに3D感が薄いので視点に合わせてコックピットの中が見えたりすると良い」 曰く「コックピットからの眺めってこんなにクリアなの?もっと薄暗かったりしないの?」 曰く「このままだとプリクラのフレームと変わらんな」 はい。ガッツリ本質を突いてきてくれました。 ガンダムを知らない奥さんからしてもこれではコックピットに居る気分は味わえないようです。 プロダクトの本質に関わる部分なので、ここは早急に改善していかなければ! とりあえず、今回の改善案としては3つほど ①コックピットの3D感を高める  ⇒ Blender使ってもっとリアルなコックピットを作る ②コックピット内で視点を動かせるようにする  ⇒ マウスや画面タッチで3Dモデルの確度が変わるようにすればできるか ③コックピットからの景色をリアルに  ⇒ コックピットの窓部分に半透明のオブジェクトを置く 今後も色んな人からフィードバックをもらいながら自分の作りたいものに近づけていけたら良いなと思っています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ガンプラの世界に没入したかった!AR.jsでコックピット視点カメラのプロトタイプを作成

30代男子。ガンプラで遊んでます! 最近、自分の部屋ができたこともあり、久しぶりにガンプラを作っています。 作ったガンプラは部屋に飾ったり写真を撮ったりしているのですが、ガンプラ自体のクオリティが昔より上がっていたり、スマホのカメラ自体もすごく高性能になっていることで、簡単にカッコイイ写真が撮れるのでテンション上がります。 (バンダイ様のおかげです。私のようなパチ組専門でもすごくカッコイイです。) 実は今度クラファンにブンドド※用のカメラデバイスを出そうとしているのですが、最近AR.jsなるもので手軽にWebでARアプリができることを知ったので、これを使って超プロトタイプ的にブンドドを更に楽しくするARアプリを作ってみました。 ※ブンドドっていうのはプラモデルやフィギュアを使ってやる所謂「ごっこ遊び」的なものです。 コックピット視点でブンドドしたいなーということで 作ってみました。 仕組みは単純でカメラ映像にAR.jsでコックピットの3Dモデルを重ねただけです。 ↓ここから遊べます。 先日WEBアプリでもARできるって知ったんで、コックピット視点でガンプラ眺めるアプリ作ってみた。(スクショも撮れるよ。)コックピット作りこんだり、カメラをリモートにしたりできると面白くなるかな。#ガンプラ#拡張現実 pic.twitter.com/BCEgMROtDc— shoito66 (@shoito66) June 12, 2021 動画はスマホで撮ったもので、PCで見るとこんな感じ。 (Webカメラのピントが上手く合わず・・・) 仕組みと作り方 今回のアプリは冒頭でも触れている通り、AR.jsというライブラリを活用したWebアプリケーションになっています。 PCやスマホのカメラで移した映像にAR.jsでコックピットの3Dモデルを重ねて表示しています。 ↓イメージ的にはこんな感じ 3Dモデルの準備 コックピットの3Dモデルはこちらのサイトで無料のものをダウンロードして使用しました。 少しブレンダーで弄りました。 ブレンダーで書き出したモデルをAR.jsで使用する方法は以下の記事にまとめてあります。 Webアプリケーションの作成 今回作成したアプリケーションのコードはこちらです。 See the Pen コックピット視点を楽しもう! by shoito66 (@shoito66) on CodePen. スタイルやスクリプトにも色々書いてありますが、これはスクリーンショットを取ったり保存したりする部分ですので、ARについてはHTMLだけで完結しています。 スクリーンショットを取る部分はこちらの記事を参考にさせていただきました。 周りの人に使ってもらってみた 今回はクラファンに向けたプロトタイプという意味も強いので、折角ならと身近な人に使ってもらいました。 使ってもらった相手は私の奥さん。 頼む前は勝手に以下の様な想像をしていました。 ・ガンダムはあまり知らないからこれを使ってブンドドの世界が楽しくなるかはあまり気にしなさそう ・エンジニアなので、起動時間やスクショ部分の使い勝手等についての意見ならもらえるかな とか思っていたら、実際に使ってもらって返ってきたフィードバックは以下のようなものでした。 曰く「コックピットに3D感が薄いので視点に合わせてコックピットの中が見えたりすると良い」 曰く「コックピットからの眺めってこんなにクリアなの?もっと薄暗かったりしないの?」 曰く「このままだとプリクラのフレームと変わらんな」 はい。ガッツリ本質を突いてきてくれました。 ガンダムを知らない奥さんからしてもこれではコックピットに居る気分は味わえないようです。 プロダクトの本質に関わる部分なので、ここは早急に改善していかなければ! とりあえず、今回の改善案としては3つほど ①コックピットの3D感を高める  ⇒ Blender使ってもっとリアルなコックピットを作る ②コックピット内で視点を動かせるようにする  ⇒ マウスや画面タッチで3Dモデルの確度が変わるようにすればできるか ③コックピットからの景色をリアルに  ⇒ コックピットの窓部分に半透明のオブジェクトを置く 今後も色んな人からフィードバックをもらいながら自分の作りたいものに近づけていけたら良いなと思っています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Youtube検索サイト(音声入力)を嫁にフィードバックをもらったらズタボロだった。

卒業制作プロダクト(↓)のフロントのイメージを掴むために、GitHubで見つけた「Yoube Search」をベースに改造してみます。 完成したサイトはこちらです。(音声からしか検索できないバグあり。。。後述します(´・ω・`)) Youtube APIを取得する 動作にはAPIが必要なので、以下のURLを参考にして取得しました。 改造元の挙動を確認する こんな感じで検索単語を入力して、「Published Date」にプルダウンで変更して「Submit」を押すと古い順に出力できました。 音声入力を可能にする 今週学んだ「Web Speech API」を組み込んで音声で検索単語を入力できるようにしてみます。 授業で学んだVue.jsを移植しようとしましたが大本がjQueryで書かれており、うまく移植できなかったので仕切り直し。 以下URLを参考に「start」と「stop」を組み込んでみました。 音声入力した値で検索してくれるようになりました。 あとはデフォルトのソートが「None」だったので「Published Date」をデフォルトに変更しています。 課題 ・文字入力欄(「Enter Channel, playlist or video」と書かれてるところ)に音声で入力したテキストを逆輸入したかったのですが時間切れでした。 ・改造する前からなのですが、Submitを2回押さないとちゃんと「Published Date」順に表示されません。。 デプロイ Netlifyへデプロイします。 使ってもらってフィードバックをもらう 嫁(非IT業界)に使ってもらいました。 ●こんな反応を期待しているという期待値 ・スマホで文字入力するのは面倒なので音声入力は助かる。 ●実際に触ってもらってどうだったか ・音声入力するのに「start」ぼたんを押さないといけないんだね ・英語なのは何か意図があるの? ・このプルダウンの「10」ってなあに? ・入力したのと違う結果が出る気がする ・入力内容を変更して「Submit」押しても更新されない ・音声入力がご認識されたときの訂正、削除方法がわからない ・音声入力の際に、STOPもおさないといけないのはめんどい……。 ・英語わからないから困る ・音声入力だと検索できたけど、文字入力欄があるにも関わらず、文字入力で検索ができないとか困る ・っていうか公式で音声検索できるよ? ・言葉を音声認識してテキスト入力欄に入るから、文字の修正もできるし、上書きもできるよ ・……あれ? っていうかこの検索メニューは何のために作ったんです?? ・音声入力させた際、「Stop」押さなくても「Submit」できる上に、音声認識は生きてるままだから、音声入力画面が大変なことになるよ!? ・音声認識の削除訂正ができないから、再検索もできない気がします(`・ω・´) フィードバックに基づいたアプリの改善案 ・「テキスト検索枠」に入力した値を「音声入力枠」で上書きしてしまうので、発話していないと「音声入力枠(NULL)」で検索されて、「テキスト検索枠」が意味をなしていない(ので変な結果が出た)。 → 設計不足とテスト不足。短時間で組むならテキスト入力欄はいっそ消してしまったほうが良かった。時間があればYoutube公式は「音声入力」で「テキスト検索枠」に文字列が入るのでそちらを参考にしたい。 ・「stop」ボタンで音声入力を止めるのは面倒 → 発話が止まったら入力終了とするオプションを使う。 ・一度音声入力するとサイトをリロードしないと初期化できない → 「start」を押したときに追記型になっているので、一度値を消して再度入力するようにする。 ・表示件数が「10」である、といった説明が不足している。 → 追加する。 ・英語なのでわかりにくい。 → 日本語にする。 まとめ 自分だったらKKD(勘と経験と度胸)で適当になんとなくで使うところが軒並み指摘されましたw でもこれが一般人の反応なんだろうな、と思います(´・ω・`) あとは「Submit」「Published Date」くらいいいかとそのままにしてましたが、いっそ英語が何もないレベルで日本語に変えたほうが一般向けには良さそうです。 このあたりは卒業制作に活かせそうです。 自分の期待値との差ですが、「音声入力より手打ち入力が慣れている」人の場合、”手が離せない”シチュエーションでの操作がメインで想定されるわけでもない限り音声入力のメリットはあまりないのかな、と思いました。 開発中の記事 コード index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta content="width=device-width, initial-scale=1" name="viewport"> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"> </script> <link href="assets/css/styles.css" rel="stylesheet"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"> </script> <link href="assets/images/favicon.ico" rel="icon" sizes="16x16" type="image/png"> <title>Yoube Search</title> </head> <body> <div class="jumbotron"> <div class="container"> <h1><code>Yoube Search</code></h1><br> 音声入力 <button id="start-btn">start</button> <button id="stop-btn">stop</button> <div id="result-div"></div> <form id="searchForm" name="searchForm"> <div class="container" id="search"> <div class="row"> <div class="col-sm-5"> <div class="input-group input-group-lg queryArea"> <input autocomplete="on" class="center-block form-control" id="query" placeholder="Enter Channel, playlist or video" type="text"> </div> </div> <div class="col-sm-1 qCount"> <div class="form-group"> <select class="form-control" id="resp"> <option> 10 </option> <option> 20 </option> <option> 30 </option> <option> 40 </option> <option> 50 </option> </select> </div> </div> <div class="col-sm-2 qCount"> <div class="form-group"> <select class="form-control" id="sort"> <option value="publishedAt"> Published Date </option> <option value="title"> Title </option> <option value="null"> None </option> </select> </div> </div> <div class="col-sm-2" id="submit"> <input class="btn btn-primary" id="submit" type="submit" value="Submit"> </div> </div><br> </div> </form> </div> </div> <div class="container"> <img id="load" src="assets/images/ripple.gif"> <ul id="results"></ul> </div> <script src="assets/js/scripts.js"> </script> </body> </html> script.js var API_KEY = '自分のAPIキー' $('#search-form').submit(function(e) { e.preventDefault(); }); $('#submit').on('click', (e) => { e.preventDefault(); return search(); }); function search() { $('#results').html(''); $('#buttons').html('') query = $('#query').val(); count = $('#resp').val(); sort = $('#sort').val(); query = finalTranscript; if (this.q == 'undefined') { this.q = query; this.s = sort; } if (this.q != query || this.c != count) { $('#load').show(); results = []; this.q = query; this.c = count; $.get('https://www.googleapis.com/youtube/v3/search', { part: 'snippet, id', q: query, maxResults: count, key: API_KEY }, function(response) { $('#load').hide(); $.each(response.items, (i, item) => { let output = getJson(item); results.push(output); }); displayResults(results); }); } else if (sort != "null") { this.results = sortResults(this.results, sort); displayResults(this.results); } else if (sort == "null") { displayResults(this.results); } } function sortResults(results, attr) { sortedResults = [] for (i = results.length - 1; i >= 0; --i) { for (j = 0; j < i; j++) { if (attr == 'title') { if (results[j].title.localeCompare(results[j + 1].title) > 0) { temp = results[j]; results[j] = results[j + 1]; results[j + 1] = temp; } } else if (attr == 'publishedAt') { if (results[j].publishedAt.localeCompare(results[j + 1].publishedAt) > 0) { temp = results[j]; results[j] = results[j + 1]; results[j + 1] = temp; } } } } return results } function displayResults(results) { $('#buttons').html('') for (each = 0; each < results.length; each++) { $('#results').append(getString(results[each])); } } function getString(output) { return `<li> <div class ="list-left"> <a href="${output.href}"><img src="${output.thumb}"></a> </div> <div class="list-right"> <a href="${output.href}"><h3>${output.title}</h3></a> <p>By <span>${output.channelTitle}</span> on ${output.publishedAt}</p> <p>${output.description}</p> </div> ` } function getJson(item) { var object = new Object(); object["channelTitle"] = item.snippet.channelTitle; object["href"] = "https://youtube.com/" if (item.id.kind == "youtube#channel") { object["href"] = object["href"] + "channel/" + item.id.channelId } else if (item.id.kind == "youtube#video") { object["href"] = object["href"] + "watch?v=" + item.id.videoId } else if (item.id.kind == "youtube#playlist") { tumb_id = item.snippet.thumbnails.default.url.split('/')[4] object["href"] = object["href"] + "watch?v=" + tumb_id + "&list=" + item.id.playlistId } object["title"] = item.snippet.title; object["thumb"] = item.snippet.thumbnails.high.url; object["publishedAt"] = item.snippet.publishedAt; object["description"] = item.snippet.description; return object } //以下WebSpeechAPI用 const startBtn = document.querySelector('#start-btn'); const stopBtn = document.querySelector('#stop-btn'); const resultDiv = document.querySelector('#result-div'); SpeechRecognition = webkitSpeechRecognition || SpeechRecognition; let recognition = new SpeechRecognition(); recognition.lang = 'ja-JP'; recognition.interimResults = true; recognition.continuous = true; let finalTranscript = ''; // 確定した(黒の)認識結果 recognition.onresult = (event) => { let interimTranscript = ''; // 暫定(灰色)の認識結果 for (let i = event.resultIndex; i < event.results.length; i++) { let transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { finalTranscript += transcript; } else { interimTranscript = transcript; } } resultDiv.innerHTML = finalTranscript + '<i style="color:#ddd;">' + interimTranscript + '</i>'; } startBtn.onclick = () => { recognition.start(); } stopBtn.onclick = () => { recognition.stop(); }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript 動画・画像関連 お試し

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

初心者がCSSでアニメーションさせる

超初心者なのですが、ハムスターを愛でるためのWEBアプリケーションを作ろうとしています。お遊びアプリには動きがないとね!!!ということで、WEBページでのアニメーションに触れてみました。ネット上で仕入れたサンプルをベースにとりえあずハムスターを動かすにはどうするのか?これを確認しつつ、アプリケーションで必要となる「現在地」から目標地点までの距離を測る機能を実装します。 出来上がったWEBアプリケーション netlify上にアップしましたのでご覧ください 画面イメージショット (実際はグレーのハムスターが右から左に横切ります) 参考)開発予定のプロダクト(note記事に飛びます) 確認したかった機能 ハムスターの画像をアニメーションさせる @keyframes でアニメーションできることがわかった 以下のサイト様の解説を参考にさせていただきました 現在地から目標地点までの距離を計測する 2点間の距離を計測するライブラリ「Geolib」で簡単に計測できることがわかった 以下のサイト様の解説を参考にさせていただきました ユーザーレビュー 家族ども(嫁・子供)にみてもらいました。コメントは以下の通り ハムスターがかわいい 背景のタッチもかわいい ハムスターがもうちょっと細かく動くといいかも いままで何をやりたいのかわからなかったがモノを見て分かった ハムちゃんの動きがすこし雑だよな、、とおもっていたところをやっぱり指摘されたのでここは要改善点。いろいろ調べて、もっとかわいらしく動くようにしたいと思う。 4点目は、やはりそうか、、という感じだが、人はモノを見せてもらうまでは、なかなか理解できないものだなと思った。引きつづき「動くもの」を素早く作り、フィードバックが得られるようになりたいと思う。 ソースコード HTML index.html <!DOCTYPE html> <html lang="ja-jp"> <head> <meta charset="UTF-8"> <title>ハム旅</title> <link rel="stylesheet" href="css/style.css"> <script src="app.js" defer></script> </head> <body> <script src="https://cdn.jsdelivr.net/npm/geolib@3.3.1/lib/index.min.js"></script> <div class="board"> <H1>はむ旅</H1> <div class = "dist"><P><img width = "50" src = "grayham.png">グレハムちゃんちまで</P></div> <H3><div id = "kyori"></div></H3> </div> </body> </html> CSS style.css body { margin: 0; } .hamu { position: absolute; left: 110%; bottom: 100px; width: 200px; height: 158px; background-image: url(../hammask.png); animation: hamuwalk 0.6s 0s infinite steps(9), hamuMove 15s 0s linear; transform-origin: 50% 100%; } @keyframes hamuwalk { 100% { background-position: -1800px 0; } } @keyframes hamuMove { 0 { left: 100px; bottom: 100px; } 100% { left: -300px; bottom: 100px; } } .board { height: 1000px; position: relative; overflow: hidden; background: rgb(0, 0, 0); background-image: url(../bg.jpg); background-size: cover; background-position: center bottom; } H1 { text-align: center; font-size: 80px; color: rgb(1, 26, 250); } H3 { text-align: center; font-size: 50px; color: rgb(0, 0, 0); } .dist { text-align: center; font-size: 40px; } Javascript app.js var board = document.querySelector(".board") function start() { var div = document.createElement("div"); div.classList.add("hamu"); div.style.transform = "scale(1.2)"; div.style.animationDuration = "5s"; board.appendChild(div); div.addEventListener("animationend", function () { this.parentElement.removeChild(this); }); } navigator.geolocation.getCurrentPosition(successCallback, errorCallback); function successCallback(position) { //geolibで距離を計測 let distance = geolib.getDistance( {latitude: position.coords.latitude, longitude: position.coords.longitude}, //自分 {latitude: 35.6895014, longitude: 139.6917337} //都庁 ); console.log(distance); document.getElementById( 'kyori' ).innerHTML = '約' + distance / 1000 + 'キロメートル'; } function errorCallback(error) { //エラー時 } start();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactにおけるコンポーネントのメモ

タグを関数ごとに分ける Reactは内部の仮想DOMレンダラーを使い、 タグを関数ごとに分けることが出来ます。 こんな感じ <div class="container mt-4"> <div id="root">wait</div> </div> <script type="text/babel" /> let dom = document.querySelector("#root") let message = "message variant" function Welcome(props) { return <div className={props.alert}> <p className={props.fontSize}>Hello React! {props.name}</p> </div> } let el =( <div> <Welcome /> </div> ) ReactDOM.render(el, dom) </script> こうすると、 function Welcome(props) {} この関数の中身がタグに描画されるみたいですよ。 もちろん、関数である以上、 引数も取れます <div class="container mt-4"> <div id="root">wait</div> </div> <script type="text/babel" /> let dom = document.querySelector("#root") let message = "message variant" function Welcome(props) { return <div> <p>Hello React!</p> </div> } let el =( <div> <Welcome name="Taro" fontSize="h2" alert="alert alert-primary" /> <Welcome name="Hanako" fontSize="h5" alert="alert alert-dark" /> </div> ) ReactDOM.render(el, dom) </script> こんな感じみたいですね 参考図書 React.js&Next.js超入門 掌田 津耶乃 https://www.amazon.co.jp/React-js-Next-js%E8%B6%85%E5%85%A5%E9%96%80-%E6%8E%8C%E7%94%B0-%E6%B4%A5%E8%80%B6%E4%B9%83/dp/4798056928
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHubで「javascript web application youtube」で検索して出てきた59件のソフトを試してみる。

卒業制作でYoutubeの検索を投げるシステムを作るので、GitHubで「javascript web application youtube」で検索して出てきた59件のソフトを見て、一部ソフトはserveして実際に使ってみます。 Sentiment_analysis_of_youtubers_webapp Sentiment analysis is the use of natural language processing, text analysis, computational linguistics, and biometrics to systematically identify, extract, quantify, and study affective states and subjective information. So, in this application, we are asking a YouTuber to enter the channel id and a particular timeline. By using the channel id and timeline we are performing sentiment analysis on his videos by fetching the subtitles of their videos in a particular timeline given by the YouTuber.Basically performing intent and emotion classification on his video subtitles. センチメント分析とは、自然言語処理、テキスト分析、計算言語学、バイオメトリクスなどを用いて、感情の状態や主観的な情報を体系的に特定、抽出、定量化、研究することです。このアプリケーションでは、YouTuberにチャンネルIDと特定のタイムラインを入力するよう求めています。チャンネルIDとタイムラインを利用して、YouTuberが指定した特定のタイムラインにある動画の字幕を取得することで、彼の動画のセンチメント分析を行っている。基本的には、彼の動画の字幕に対して意図と感情の分類を行う。 感触 動画が準備されており、わかりやすいです。 ユーザー登録、メール送付によるベリファイまであります。 Youtubeのチャンネルと期日を入力すると、その期間の感情値が棒グラフで表示されます。 手元でserveしましたがindexページが出ず、ちょっと後回しです。(他にもこうなるのできっとやり方がわかってない) fdd-youtube-searcher Fast search for your favorite youtube videos, in an easy to use UI 使いやすいUIで、お気に入りのyoutube動画を高速検索 Working prototype of a youtube searcher web application, built with ReactJs, Youtube API ReactJsとYoutube APIで構築された、youtubeサーチャーウェブアプリケーションのワーキングプロトタイプ 感触 gitからのインストールは失敗。 $ git clone git@github.com:fernandedios/fdd-youtube-searcher.git Cloning into 'fdd-youtube-searcher'... The authenticity of host 'github.com (13.114.40.48)' can't be established. RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8. This key is not known by any other names Are you sure you want to continue connecting (yes/no/[fingerprint])? y Please type 'yes', 'no' or the fingerprint: yes Warning: Permanently added 'github.com' (RSA) to the list of known hosts. git@github.com: Permission denied (publickey). fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. ZIPをダウンロードして「npm install」して「npm start」するとエラー。 PS C:\Users\須藤晋介\Downloads\fdd-youtube-searcher-master> npm start npm ERR! Invalid name: "FDD Concept: Youtube Searcher" npm ERR! A complete log of this run can be found in: npm ERR! C:\Users\須藤晋介\AppData\Roaming\npm-cache\_logs\2021-06-12T00_10_48_427Z-debug.log logを見てもちょっとわからなかったので中止。 React-Django_YoutubeCommentAnalysis Web Application that classifies youtube comments as positive and negative with react frontend and DRF as backend. Classification is done by the NaiveBayes Classifier in the Natural Language ToolKit (NLTK). Currently classification is erroneous as the classifier uses the bag of words model and therefore doesn't account complexity in natural language. With time as I learn more about Machine Learning and Natural Language Processing, I hope to improve this classifier and make it's accuracy as close as possible to a human. Classifier is saved as Classifier.sav using pickle to prevent repetitive training of model. Model is currently trained on twitter_samples and movie_reviews corpus found in NLTK This web app also uses the Youtube Data API to get comments of a particular video. Therefore, to use the App, one requires creating a client_secret.json (OAUTH) file using the Google API console for the Youtube Data API. Place the file in the backend folder. The app's backend is written in Python using the Django Rest Framework. The backend is responsible for making API calls to the Data API as well as classifying the incoming comments as positive or negative using the classifier. The backend then returns a JSON body for consumption by the frontend. (REST API) The app's frontend uses React. The frontend takes the URL from youtube as input for the backend and displays the consumed JSON. Green stands for positive comment. Red stand for Negative comment. If number of positive and negative comments are equal, the background color of the page becomes yellow. フロントエンドにreact、バックエンドにDRFを用いて、youtubeのコメントを肯定的なものと否定的なものに分類するWebアプリケーションです。 分類は、Natural Language ToolKit(NLTK)のNaiveBayes Classifierによって行われます。 現在のところ、分類器はbag of wordsモデルを使用しているため、自然言語の複雑さを考慮しておらず、分類に誤りがあります。 今後、機械学習や自然言語処理について学びながら、この分類器を改良し、人間に近い精度を実現したいと考えています。 分類器は、モデルの繰り返し学習を防ぐために、pickleを使ってClassifier.savとして保存しています。 モデルは現在、NLTKで見つかったtwitter_samplesとmovie_reviewsのコーパスで学習されています。 また、このウェブアプリは、Youtube Data APIを使用して、特定のビデオのコメントを取得します。 そのため、このアプリを使用するには、Youtube Data API用のGoogle APIコンソールを使用してclient_secret.json(OAUTH)ファイルを作成する必要があります。 このファイルをbackendフォルダに配置します。 アプリのバックエンドは、Django Rest Frameworkを使ってPythonで書かれています。 バックエンドは、Data APIへのAPIコールを行うとともに、分類器を使用して入力されたコメントをポジティブまたはネガティブに分類します。 バックエンドは、フロントエンドが利用できるようにJSONボディを返します。(REST API) アプリのフロントエンドはReactを使用しています。 フロントエンドは、バックエンドの入力としてyoutubeからのURLを受け取り、消費されたJSONを表示します。 緑はポジティブなコメントを表します。 赤は否定的なコメントを表しています。 肯定的なコメントと否定的なコメントの数が同じであれば、ページの背景色は黄色になります。 感触 起動できず。 ここで「あ、Reactって書いてるわ」と気づいて見様見真似でインストール。 $ npm install --save-dev webpack webpack-cli webpack-dev-server $ npm install -g webpack webpack-cli $ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader $ npm install --save-dev react react-dom $ ./node_modules/.bin/webpack-dev-server --content-base src --mode development しかし詰まる。後回し。 ReactYouTubeSearch A youtube search web application built in React, using the youtube api. Reactで構築されたyoutube検索ウェブアプリケーションで、youtube apiを使用しています。 Project Completed from Stephen Grider Course Modern React with Redux on Udemy TranscryptApp A web application for simple YouTube video transcript downloads. YouTubeの動画を簡単にダウンロードするためのWebアプリケーションです。 Contribution to this project is allowed. If there are any features that you think should be included, create an issue or a pull request with the completed feature. This project uses Semantic Release, therefore use of the Angular commit message convention is required. Read the commit conventions here. If the commit convention isn't used, the pull request will be closed without merge. このプロジェクトへの貢献は認められています。もし含まれるべきだと思われる機能があれば、完成した機能を伴ったissueやpull requestを作成してください。 このプロジェクトではセマンティックリリースを使用しているため、Angularのコミットメッセージ規約を使用する必要があります。コミット規約はこちらをご覧ください。コミット規約が使用されていない場合、プルリクエストはマージされずに閉じられます。 感触 インデックスページ開けないやつ。これは私が根本を理解してないやつな気配。 Youtube-downloadr-JS this is our node js based web application where we are using youtube-dl library of node npm to import some functions of youtube stream generation and to provide us the video(and many more things) in javascript code format. NOTE : THIS MICRO PROJECT IS UNDER CONSTRUCTION HOOD, YOU CAN CONTRIBUTE LADIES AND GENTLEMEN. これは私たちのnode jsベースのウェブアプリケーションで、node npmのyoutube-dlライブラリを使用して、youtubeストリーム生成のいくつかの機能をインポートし、javascriptコード形式でビデオ(およびその他多くのもの)を提供しています。 注:このマイクロプロジェクトは、フードを構築中で、あなたは紳士淑女に貢献することができます。 感触 見本サイトが公開されていたのでそこに「YoutubeのURL」「動画のIDだけ」を入れてみたが動作せず。諦める。 Translatefyer A quick and dirty web application for 'translatefying' (running throught machine translation multiple times) any text. Heavily inspired by the Google Translate Sings videos by the YouTube channel Translator Fails. Check it out live here. (I'm using Heroku's free plan so the site might take a while to load.) 任意のテキストを「翻訳する」(機械翻訳を何度も実行する)ためのクイック&ダーティなウェブアプリケーションです。YouTubeチャンネル「Translator Fails」の「Google Translate Sings」のビデオに強く影響を受けています。 ここでライブでチェックしてみてください。(Herokuの無料プランを使っているので、サイトの読み込みに時間がかかるかもしれません。) 感触 おっとYoutubeは関係ないアプリだった。 でも機械翻訳を何度もやるのは面白そうなのでメモ。 Yoube Search A web application to access YouTube v3 Data API and to sort the data at client side with respect to Title and Published Date. YouTube v3 Data APIにアクセスし、クライアント側でタイトルや公開日を基準にデータをソートするためのWebアプリケーションです。 感触 シンプルで改造するには良いかも。 serveで起動した。検索入れてもずっとloading... →あ、API Keyか! API Keyを取得して置き換えた。手順は以下URLを参考にさせていただきました。 さて、リトライします。 →チャンネルは検索結果が出てきました。ただ、「Published Date」で1回目に出てきたときのソートは「None」(人気順?)で、もう1度「Submit」を押したらちゃんと「Published Date」になる動きをしてる気がする。 →再生リストは「URL」「リスト番号だけ」のどちらもできず。 →もういっちょ別の再生リストを投入。出るには出たけど1動画しか出ない。。。 →単語検索は出来ました。  投稿日時順も出来ました。(でもこれ降順に欲しいな…あとでコードみて書き換えてみよう)  一度「Published Date」で検索しちゃうど2度目に「None」を選んでもソートが変わらないのはバグかな。 UFOAbductionRiskAnalyzer UFO Abduction Risk Analyzer is a Web application for finding one's risk of abduction based on real time user location. Website plots all the UFOs from National UFO Reporting Center (NUFORC) on Google maps. Each UFO object provides necessary details like shape, location and weather details. It provides abduction details from YouTube, Blogger, Fli… UFOアブダクション・リスク・アナライザーは、リアルタイムのユーザーの位置情報に基づいて、自分のアブダクションのリスクを見つけるためのウェブアプリケーションです。このウェブサイトは、National UFO Reporting Center (NUFORC)からのすべてのUFOをGoogleマップ上にプロットしています。それぞれのUFOオブジェクトは、形状、位置、天候の詳細など、必要な情報を提供します。YouTube、Blogger、Fli...からアブダクションの詳細を提供します。 感触 UFOアブダクションリスクアナライザーという語感が強すぎて最高。X-Files世代直撃。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTML/CSS/Javascript/JSXのコメントアウトの方法

はじめに コメントアウトの方法をよく忘れるのでまとめておきます こちら[1][2]を参考にさせていただきました。 2021/6/13追記 コメントで指摘されたHTMLのコメントアウトについて修正しました。 コメントアウト 言語 1行コメントアウト 複数行コメントアウト HTML <!–- コメント -–> 左と同じ CSS /* コメント */ 左と同じ javascript //コメント /* コメント*/ JSX {//コメント} {/* コメント*/} おわりに HTMLとCSSのコメントアウトめんどくさい。 それではまた。 参考文献 [1]:【HTML】コメントアウトの書き方!注意点や便利な使い方も紹介 [2]:今更聞けない JSX のコメントアウトの構文
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactのHelloWorldはどのようにできているのかを解析する

はじめに 前回の記事[1]で最小構成のreactのHelloWorldをデプロイした。 言われるがままやっただけで理解できていないので、1つ1つ理解していく。 知識としてはprogateでHTML/CSS/javascript/Reactをやった程度。 正直Reactがどういうシステムなのか分かっていないので、現段階ではどのように動かすか理解できればいい。 なぜそうなるのかはあとで後日考察する。 プログラム index.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <!--ディスプレイに合わせて表示--> <meta name="viewport" content="width=device-width, initial-scale=1" /> <!--アドレスバーの背景色を変更(OS/ブラウザに依存)--> <meta name="theme-color" content="#000000" /> <title>React App</title> </head> <body> <div id="root"></div> </body> </html> index.js import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); App.js function App() { return ( <div className="App"> Hello,World </div> ); } export default App; 解析 コードの参照順としてはindex.html→index.js→App.jsとなっている。indexの部分はどのプログラムにもありそうだが、App.jsは他の名前とかにするんだろうか。最小構成だからこれで済んでいるが、おそらくApp.jsで他の作成したコンポーネントを呼び出してさらにネストしていくのだと思われる。 index.html headタグはコメントに書いてある通り。 普通のhtmlだが、divタグにid="root"とある。 <div id="root"></div> これがあとあと必要な処理のよう。 index.js import React from 'react'; import ReactDOM from 'react-dom'; importはpythonとかでも似たようなことしてますね。 厳密な構文を探したところ、このサイト[2]に載ってました。 import {インポートするエクスポートの名前} from "モジュールの名前"; 中括弧はexport defaultされたものにはいらないようです[3] エクスポートはおそらくApp.jsの最後にもあったやつ。 これも調べたところ載ってました[4]。 とりあえずJavaScript モジュールを作成するために使用する文という認識で良さそうです。 export default モジュールの名前; 何がデフォルトなのかを調べたところ、こちらのサイト[5]に載ってました。 このjavascriptファイルのデフォルトとして後ろに続く名前のモジュールをエクスポートしますよという意味のよう。 あくまでモジュールの名前なのでエクスポートの名前はimport側で勝手に変えれるよう。 実施にエクスポート名をApp以外にしても普通に動作しました。 長くなりましたが1文目はreactというモジュールをReactという名前でインポートしますよという意味。 となると別にReactの部分は変えても良さそうだが、慣習的に同じにしてるってことだろう。2文目も同様。 ただ、こういうパッケージはnpmかyarnでプロジェクトディレクトリ内にnodemodules内にダウンロードされています。 特に指定しなかったらnodemodulesからとってくるという認識でいいのかな? import App from './App'; さっきとほぼ同じですが、なんかパスの指定っぽくなってますね。 [2]に全く同じような構文はなくて、ファイルの拡張子まで指定しているやつはあります。 import {App} from './App.js'; これなら構文通りなんですが微妙に違います。 思うにこれはモジュールじゃなくてファイルそのものからインポートしてきています。 App.jsではモジュール名をAppとしてエクスポートしているので import 自分の好きな名前 from "モジュール名までのパス"; みたいな感じじゃないでしょうか。 App.jsをディレクトリに入れてパスを変更したところちゃんと表示されました。 パスを指定しなければnodemodulesを見に行って、それ以外は相対パス指定してねみたいな感じでしょうか。 また気になったので./App.jsに変えてみたところ動作しました。 これはファイルそのものを参照しても大丈夫ということなのか、ただ拡張子省略できますよのどっちの意味なんだろう。 これ以上突っ込むと沼にはまりそうなので拡張子は省略して書けますということにしておきましょう。 ReactDOM.render(); これが全く分かりません。 なんか出力してるんだろうなーぐらいの認識です。 公式リファレンス[6]によるオプションを省いた構文を載せます。 ReactDOM.render(element, container) リファレンスでは渡されたcontainerのDOMにReact要素をレンダーするという風に書いてあります。 まずレンダリングの言葉の定義を考えます。 [7]の内容の一部を下記に要約引用します。 レンダリングとは変更前と変更後の仮想DOMの差分を比較し、差分を検知することで仮想DOMを再構築すること。 DOMには変更が直接反映されるリアルDOM(たぶん仮称)と仮想DOMが存在する。 Reactでは2つの仮想DOMがあり、その差分をリアルDOMに反映させる。 要するにこの関数を用いて差分を実際のDOMに反映させているということでしょう。 よくよく考えてみれば結局HTML/CSS/javascriptなんだからそのファイルをいじることが出力の変更になる。 ずっとCでprintfしたりC++でcoutしてたので盲点だった。 つまりこの関数は実際のDOMを変更するための関数という認識でいいでしょう。 ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); とするとこのReact.StrictModeで囲まれているのがReact要素(component)、document.getElementById('root')がcontainerだ。 [8]によると問題のある書き方を特定するためのモードのよう。 gcc -Wallみたいな感じだろうか。 とりあえずこれはこういうものだと思ってとりあえずつけておけば良さそう。 そうなるとが残るのでこれがReact要素ということでしょう。 この関数は構文上複数のコンポーネントを持てない。 だからApp.jsのようなコンポーネントを集約するためのコンポーネントを作成してこの関数にまとめて渡すのでしょう。 次はdocument.getElementById('root')です。 まずはこの関数の構文が知りたいので調べたところ、こちらに[9]載っていました var element = document.getElementById(id); idを引数としてElementオブジェクトを返すようです。 Element オブジェクトは調べましたがよく分かりません。 要するにこのidで特定された場所を返すような関数なんでしょう。 今気づきましたがCの癖で関数と呼んでますがメソッドですね。 正直かなり違いますが直すのめんどくさいので次から直すので許してください。 となるとcomponentは各コンポーネントの内容、containerはそれをどこに挿入するかを表してるみたいな感じでしょうか。 たぶん正確には違うと思いますが、構成を理解してプログラムが書ければいいのでこういう理解でいいでしょう。 そうなるとindex.htmlとindex.jsはほとんどいじらなくて良さそうですね。 こういうこと誰も書いてないですが、みんな理解せずになんとなくで書けるんでしょうか? おまじないにしても最低限理解できないとフォルダ構成とかも変えられない気がするんですが。 しかし、こうやって見てみると見事なまでにカプセル化されてますね。 1つのファイルが1つの機能を持つようにという意思も感じられます。 なんでこんなにファイルを分けるんだと思ってましたが、こういったポリシーがあったのでしょうね。 App.js export default App; これは先ほど説明した通りデフォルトでAppというモジュール名でエクスポートするという文です。 Reactの方針として1つのファイルには1つの機能が理想でしょうから、基本的にはデフォルトでいい気がしますね。 function App() { return ( <div className="App"> Hello,World </div> ); } 関数Appを定義しています。 返り値としてDOMを返しているということでしょう。 progateでは class App extends React.Component { } となっていたのですが、関数でもいいんでしょうか。 まあそこは実際に作る際に考えます。 おわりに 今回はrecctのHelloworldはどのように作られているのか解析しました。 やっとスタートラインに立てた気がします。 Reactではjavascriptよりも強くオブジェクト指向が意識されていますね。 それではまた。 参考文献 [1]:GitHub Pages で最小構成のreactのhelloworldをデプロイしてみた [2]:import [3]:ES2015 (ES6) の import や export (default)(React基礎講座4) [4]:export [5]:export defaultってなんだろう [6]:ReactDOM [7]:Reactのレンダリングと描画の違いを整理する [8]:React.StrictModeがあぶり出す意外な問題点 [9]:Document.getElementById()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JS]mapとforEachの違い

はじめに JavaScript初学者です。 mapとforEachの違いについて学んだので備忘録として残しておきます。 公式ドキュメント それぞれ公式ドキュメントから説明を引用させていだだきます。 mapの場合 map() メソッドは、与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成します。 foreachの場合 forEach() メソッドは与えられた関数を、配列の各要素に対して一度ずつ実行します。 要するに2つの違いは新しい配列を返すか返さないかということになります。 具体例 forEachの場合 const Array = [1,2,3,4,5] console.log(Array.forEach(x => x * 2 )) => undefined forEachは処理するだけなので返り値は何もありません mapの場合 const Array = [1,2,3,4,5]; console.log(Array.map(x => x * 2 )) =>  [2, 4, 6, 8, 10] mapは処理をした上で新しい配列を返してくれます。 もしforEachで新しい配列を作りたい場合は以下のようになります。 const Array = [1,2,3,4,5]; const newArray = []; Array.forEach( x => newArray.push(x * 2) ); console.log(newArray) => [2, 4, 6, 8, 10] まとめ 今回はmapとforEachを比較しましたがmapの方がコードがシンプルかつ新しい配列を返してくれるので便利だし実際mapの使用率の方が高そうです。 しかしforEachの方が処理速度が早いらしいです、また新しい配列を返さないので使い分け考えてコードを書いてみるのも良いかもしれませんね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cannot find module '@emotion/styled' from 'node_modules/@material-ui/styled-engine/node/index.js'のエラーの解決策

@emotion/styledのError 以下のようなErrorが出たので解決策をメモがわりに記載します。 Cannot find module '@emotion/styled' from 'node_modules/@material-ui/styled-engine/node/index.js' 解決策 npm install @emotion/react @emotion/styled yarnを入れている人なら yarn add @emotion/react @emotion/styled 該当部のpackage.jsonは以下になります。 "peerDependencies": { "@emotion/react": "^11.0.0", "@emotion/styled": "^11.0.0", "react": "^17.0.0" }, 多分、受講でこのようなErrorは指定のバージョンでやれば出ないと思いますが、 私は最新版のバージョンでやろうとしたため出たものだと思われます。 Module not found: Can't resolve '@emotion/react' のErrorであれば 以下のstack overflowさんの記事が参考になると思います。 https://stackoverflow.com/questions/65486256/module-not-found-cant-resolve-emotion-react
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

D3.jsで表をつくる

データ可視化ライブラリD3.js v5を使って表をつくる方法をまとめます 1. d3のDOM操作 1-1. 対象を選択する 単独の対象を選択する d3.select()がやってくれます divタグを選択する d3.select('div') idがhogeである対象を選択する d3.select('#hoge') 複数の対象をすべて選択する d3.selectAll()がやってくれます divタグをすべて選択する d3.selectAll('div') 続けて選択する 連ねて書くと最初に指定した条件のタグからスタートして内側を検索できます id="hoge"のタグ内にあるdivタグを選択する d3.select('#hoge').selectAll('div') 1-2. 操作する divタグを追加する d3.append('div') class='hoge'を追加する d3.attr('class', 'hoge') styleを追加する d3.style("color", "red") タグ内に文字列を書き出す d3.text('fuga') 指定したタグを消す d3.remove() イベントを追加する d3.on('click', function(){}) 1-3. 実装例 ここまでのメソッドを使って実際に動かしてみます test.html <head> <script src="https://d3js.org/d3.v5.min.js"></script> </head> <body> <div id="chart"></div> </body> <script> d3.select("#chart") .append("div") .attr("class", "hoge") .style("color", "red") .text('いっぽんでもにんじん'); .on('click', function(){ console.log("clicked"); }); </script> 上記をブラウザ上で開くと以下のように書き換わって表示されます 表示されるhtml <body> <div id="chart"> <div class="hoge" style="color: red;">いっぽんでもにんじん</div> </div> </body> また、文字列が表示されている部分をクリックするとclickイベントを拾ってconsole.log("clicked")してくれます こんな感じでシンプルにDOM操作を記述できます 1-4. 配列を展開する d3.data()に配列やオブジェクトを渡してやると、操作対象とデータを重ねた状態にしてくれます。ここがd3のすごいところでもあり、ちょっと分かりにくい部分でもあると思います 配列を展開する d3.data(['1', '2', '3']) このdata()で受け取った内容をどう扱うかについてenter, update, exitの3種類の扱い方があります こちらの記事のコードが分かりやすかったのでお借りしてやってみましょう https://kita-note.com/d3-basic-001 enter()でタグを追加する enter()した場合は該当タグが存在しないときに指定通りに追加します 例えば、以下のようにした場合、id='chart'であるタグ内にあるdivタグ内にdivタグを足して、書き出す文字列を配列から受け取った値を使って決めるという挙動になります。何故selectAll('div')した上でappend('div')しないといけないのかよく分かってないんで誰か教えてほしいんですが、どちらか消すと動かなくなります text()の引数に関数を与えた場合、d3が第1引数に値、第2引数に順番を入れて実行した結果を書き出してくれます。また、以下のように第2引数を省略して第1引数だけ受け取ることも出来ます。 <head> <script src="https://d3js.org/d3.v5.min.js"></script> </head> <body> <div id="chart"></div> </body> <script> const data = ['1', '2', '3']; d3.select("#chart") // 既に書いてあるid='chart'のタグをこれで選択 .selectAll("div") // まだ存在してないdivタグを指定 .data(data) // 配列を渡す .enter() // enter() .append("div") // divタグを足す .style("color", "red") // styleを足す .text(function(d) { return 'div:' + d; }); // 文字列を書き出す </script> こんな感じになります <body> <div id="chart"> <div style="color: red;">div:1</div> <div style="color: red;">div:2</div> <div style="color: red;">div:3</div> </div> </body> 3つ入った配列を渡したのでdivタグが3つ作成されています enter()では更新がかからない では、以下のようにdivタグが1つある状態でenter()するとどうなるでしょうか test.html <head> <script src="https://d3js.org/d3.v5.min.js"></script> </head> <body> <div id="chart"> <div></div> </div> </body> <script> const data = ["1", "2", "3"]; d3.select("#chart") .selectAll("div") .data(data) .enter() .append("div") .style("color", "red") .text(function(d) { return "create div element:" + d; }); </script> enter()は既にあるタグについては更新が掛からないので以下のようになります <body> <div id="chart"> <div></div> <div style="color: red;">create div element:2</div> <div style="color: red;">create div element:3</div> </div> </body> updateで既に存在するタグを更新する enter()を省略すると更新のみ行うupdateになります test.html <head> <script src="https://d3js.org/d3.v5.min.js"></script> </head> <body> <div id="chart"> <div></div> </div> </body> <script> const data = ["1", "2", "3"]; d3.select("#chart") .selectAll("div") .data(data) .append("div") .style("color", "red") .text(function(d) { return "create div element:" + d; }); </script> updateではタグの追加は行われないので以下のようになります <body> <div id="chart"> <div style="color: red;">create div element:1</div> </div> </body> merge()で追加と更新を両方やる 追加しつつ更新もやりたい場合は両方書けば出来るわけですが、ダサいですし内部処理的にも重複して遅くなっちゃいそうです <head> <script src="https://d3js.org/d3.v5.min.js"></script> </head> <body> <div id="chart"> <div></div> </div> </body> <script> const data = ["1", "2", "3"]; d3.select("#chart") .selectAll("div") .data(data) .enter() .append("div") d3.select("#chart") .selectAll("div") .data(data) .style("color", "blue") .text(function(d) { return "create div element:" + d; }); </script> そんなときはmerge()で両方をまとめて書けば処理的にも最適化するというのが出来るようです <head> <script src="https://d3js.org/d3.v5.min.js"></script> </head> <body> <div id="chart"> <div></div> </div> </body> <script> const data = ["1", "2", "3"]; const data_div = d3.select("#chart") // 共通部分のselectionを作っておきます .selectAll("div") .data(data) data_div.enter() // enter().append()したものとupdateで実行するものをmerge()します .append("div") .merge(data_div) .style("color", "red") .text(function(d) { return "create div element:" + d; }); </script> 上手くいったようです <body> <div id="chart"> <div style="color: red;">create div element:1</div> <div style="color: red;">create div element:2</div> <div style="color: red;">create div element:3</div> </div> </body> exit()で削除する データが2件でタグが既に3つあるような場合、不要なタグを消す操作をexit().remove()がやってくれます <head> <script src="https://d3js.org/d3.v5.min.js"></script> </head> <body> <div id="chart"> <div style="color: red;">remove div element:1</div> <div style="color: red;">remove div element:2</div> <div style="color: red;">remove div element:3</div> </div> </body> <script> const data = ["1", "2"]; const data_div = d3.select("#chart") .selectAll("div") .data(data) .style("color", "red") .text(function(d) { return "update div element:" + d; }) .exit().remove() </script> <body> <div id="chart"> <div style="color: red;">update div element:1</div> <div style="color: red;">update div element:2</div> </div> </body> 1-5. オブジェクトを展開する オブジェクトを要素とする配列を展開する場合もdata()を使えます test.html <head> <script src="https://d3js.org/d3.v5.min.js"></script> </head> <body> <div id="chart"> </div> </body> <script> const data = [ { "name": "A", "num": 1 }, { "name": "B", "num": 2 }, { "name": "C", "num": 3 } ] const data_div = d3.select("#chart") .selectAll("div") .data(data) .enter() .append("div") .style("color", "red") .text(function(d, idx) { return idx + " th num: " + d["num"] + ": name: " + d["name"] ; }) </script> d3でCSVを開くと上記のような形式で出てくるので、CSVを読んで表を書くような場合は同じやり方で展開することが出来ます 1-6. CSVファイルを読み込む d3.csv()でURLまたはPATHを指定すれば読んできてくれます d3.csv(ここにURLかPATHを書く, function(row, i){ return { index: i, data: row } }).then(function(data) { console.log(data); }) 上記のコードで下のようなCSVファイルを開くと1行ごとにオブジェクトに入れた配列にしてくれます CSVファイル a, b, c 1, 1, 1 2, 4, 8 3, 9, 27 読んできたdata [{'a': 1, 'b': 1, 'c': 1}, {'a': 2, 'b': 4, 'c': 8}, {'a': 3, 'b': 9, 'c': 27}] plotly.jsのサンプルページにあったCSVファイルをお借りして表示してみると以下のように出来ます <head> <script src="https://d3js.org/d3.v5.min.js"></script> </head> <body> <div id="chart"></div> </body> <script> d3.csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv', function(data, i){ return { index: i, data: data } }).then(function(data) { d3.select("#chart") .selectAll("div") .data(data) .enter() .append("div") .text(function(d) { return d["data"]["country"] + ", " + d["data"]["year"] + ", " + d["data"]["gdpPercap"]; }); }) </script> この調子でグラフや表に流し込んで表示することも出来ます 1-7. アロー関数を使うときは注意 なお、値を展開するときに使う関数をアロー関数で書くと思ったような動作にならないことがあるようです。これはアロー関数がthisを束縛しない性質によるようで、d3.selectionに渡す関数を書くときはアロー関数にしない方が無難っぽいです。 2. 表をつくる というのを踏まえて表を作ってみましょう こちらのコードが分かりやすかったのでお借りしてやってみます https://wizardace.com/d3-table/ <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>sample</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> <script src="https://d3js.org/d3.v5.min.js"></script> </head> <body> <script> var dataset = [ { "name": "A", "para1": 0, "para2": 5 }, { "name": "B", "para1": 1, "para2": 6 }, { "name": "C", "para1": 2, "para2": 7 }, { "name": "D", "para1": 3, "para2": 8 }, { "name": "E", "para1": 4, "para2": 9 } ] const names = d3.keys(dataset[0]); function make_table(div){ const table = d3.select("body") .append('div') .attr('class', 'table-responsive') .append("table") .attr('class', 'table table-light') // table-striped table-hover table.append("thead") .append("tr") .selectAll("th") .data(names) .enter() .append("th") .text(function(d) { return d; }); table.append("tbody") .selectAll("tr") .data(dataset) .enter() .append("tr") .selectAll("td") .data(function(row) { return d3.entries(row); }) .enter() .append("td") .text(function(d) { return d.value; }) let selected = Array(dataset.length).fill(false); const index = Array.from({length: dataset.length}, (_, i) => i); const activated = [null, 'table-active']; d3.select('tbody') .selectAll("tr") .data(index) .on('click', function(i) { selected[i] = !selected[i]; d3.select(this).attr('class', activated[selected[i] * 1]); }); return selected; } function remove_table(){ const div = d3.select('body') .select('div') .remove(); } function update_table(){ remove_table(); make_table(); } let selected = make_table(); </script> </body> </html> やってる事はベタなDOM操作です 生成されたhtml <div class="table-responsive"> <table class="table table-light"> <thead> <tr> <th>name</th> <th>para1</th> <th>para2</th> </tr> </thead> <tbody> <tr> <td>A</td> <td>0</td> <td>5</td> </tr> <tr> <td>B</td> <td>1</td> <td>6</td> </tr> <tr> <td>C</td> <td>2</td> <td>7</td> </tr> <tr> <td>D</td> <td>3</td> <td>8</td> </tr> <tr> <td>E</td> <td>4</td> <td>9</td> </tr> </tbody> </table> </div> 完成です イベントハンドラも簡単に付けられるのが素晴らしいですね 3. 表とプロットを組み合わせる Plotly.jsのプロットと組み合わせて選択対象をクリックイベントで切り替えるように作ってみます event_handler_plot.html <head> <title>EventHandlerサンプル</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> <link href="https://fonts.googleapis.com/css?family=Amatic+SC:700 rel="stylesheet"> <script src="https://d3js.org/d3.v5.min.js"></script> <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> <style type="text/css"> h1 {text-align:center; color:#337; font-family: 'Amatic SC', cursive; padding-top: 40; padding-bottom: 50;} p {text-align:center; font-family: 'Amatic SC', cursive; font-size: 200%; padding-left: 5%;} </style> </head> <body> <h1>CLICK to change enable/disable</h1> <div class="container"> <div class="row"> <div id="table" class="col-6"><!-- table here --></div> <div id="myDiv" class="col-6"><!-- Plotly chart here --></div> <div id="msg1"><p></p></div> <div id="msg2"><p></p><p></p></div> </div> </div> </body> <script src="./js/main.js"></script> ./js/main.js const dataset = [ { "name": "A", "x": 1, "y": 5 }, { "name": "B", "x": 2, "y": 2 }, { "name": "C", "x": 3, "y": 7 }, { "name": "D", "x": 4, "y": 4 }, { "name": "E", "x": 5, "y": 9 } ]; /////////////////////////////////////////////////////////// const activated = [null, 'table-active']; let selected = [false, true, true, false, false]; function get_xy(dataset, key) { return dataset.map((row) => { return row[key] }); } function get_x(dataset) { return get_xy(dataset, 'x') } function get_y(dataset) { return get_xy(dataset, 'y') } function select_x(x, selected) { return x.filter(function(_, i) { return (selected[i]); }); } function mean(x) { return Math.round(d3.mean(x)*100)/100 } /////////////////////////////////////////////////////////// function make_plot(dataset, selected) { console.log('make_plot()'); const myPlot = document.getElementById('myDiv'), data = [{ x:get_x(dataset), y:get_y(dataset), visible: selected, type:'scatter', mode:'markers', marker:{size:20} }], layout = {hovermode:'closest', height: 300, margin: { t: 20, l: 30, r: 30 } }; Plotly.newPlot('myDiv', data, layout); myPlot.on('plotly_click', function(data){ console.log('plotly_clicked'); const i = data.points[0].pointIndex update(selected, i, activated); }); } /////////////////////////////////////////////////////////// function make_table(dataset, selected, activated){ console.log('make_table()'); const names = d3.keys(dataset[0]); const table = d3.select("#table") .append('div') .attr('class', 'table-responsive') .append("table") .attr('class', 'table table-light') // table-striped table-hover table.append("thead") .append("tr") .selectAll("th") .data(names) .enter() .append("th") .text(function(d) { return d; }); table.append("tbody") .selectAll("tr") .data(dataset) .enter() .append("tr") .selectAll("td") .data(function(row) { return d3.entries(row); }) .enter() .append("td") .text(function(d) { return d.value; }) const activated_arr = selected.map((b) => { return activated[b*1]; }); d3.select('tbody') .selectAll("tr") .data(activated_arr) .on('click', function(_, i){ console.log('table clicked'); update(selected, i, activated); }) } /////////////////////////////////////////////////////////// function update(selected, i, activated) { update_selected(selected, i); update_table(selected, activated); update_plot(selected); update_msg(dataset, selected) } function update_selected(selected, i) { selected[i] = !selected[i]; console.log(selected); } function update_plot(selected) { const color = ['#ccc', '#77f'] const colors = selected.map((select) => color[select * 1]) const update = {'marker':{color: colors, size: 20}}; Plotly.restyle('myDiv', update); } function update_table(selected, activated) { const activated_arr = selected.map((b) => { return activated[b*1]; }); d3.select('tbody') .selectAll("tr") .data(activated_arr) .attr('class', function(d){ return d; }); } function update_msg(dataset, selected){ const x = select_x(get_x(dataset), selected); const y = select_x(get_y(dataset), selected); d3.select('#msg1') .selectAll('p') .text(x.length + ' points selected') d3.select('#msg2') .selectAll('p') .data([['x', x], ['y', y]]) .text(function(values){ return values[0] + ' average: ' + mean(values[1]); }) } /////////////////////////////////////////////////////////// make_table(dataset, selected, activated); make_plot(dataset, selected); console.log(selected); update_table(selected, activated); update_plot(selected); update_msg(dataset, selected) 出来ました d3でやるんだからプロットもd3で作れば良かったんですが、都合でPlotly.jsにしたかったのでこんな感じになりました まとめ d3.jsめっちゃ便利 みんなも使おう 参考にしたサイト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【アルゴリズム】JavaScriptでフィボナッチ数列問題を解く

はじめに Qiitaの競技プログラミング研究月間ということで、アルゴリズムの記事を書いています。 今回はフィボナッチ数列問題をまとめました。 JavaScriptでアルゴリズムの勉強をされている方の参考になれば幸いです。 記事を順次まとめていきますので、その他の記事についてはマイページからご覧ください。 フィボナッチ数列 フィボナッチ数列のn項目を求めてください。 fib(4) === 3 解答 2つの解法を試してみました。 以下では、n項目までのフィボナッチ数列を計算して、配列resultに格納しています。 index.js function fib(n) { const result = [0, 1]; //初期状態 for (i = 2; i <= n; i++) { const a = result[i - 1]; const b = result[i - 2]; result.push(a + b); } return result[n]; } 以下は再帰関数を使って解く方法です。 index.js function fib(n) { if (n < 2) { return n; } return fib(n - 1) + fib(n - 2); } これをテストしてみると、n=3では1msしかかかりませんが、n=15だと588msもかかってしまいます。 1つ目の解法だとforループを1回まわすだけなので計算量がO(N)で済むのですが、再帰関数だと同じ計算を何度も行うので、引数nの値が大きくなるほど計算量が膨大になってしまいます。 PASS fib/test.js ✓ Fib function is defined (2 ms) ✓ calculates correct fib value for 1 ✓ calculates correct fib value for 2 ✓ calculates correct fib value for 3 (1 ms) ✓ calculates correct fib value for 4 ✓ calculates correct fib value for 15 (588 ms) 再帰関数を使ったフィボナッチ数列の計算量を減らすためには、同じ引数に対する答えをメモ化します。 例えば、fib(6)を実行するとfib(4)が2回実行されますが、fib(4)が1回実行された時点でその答えをどこかに格納(メモ化)するようにします。 そして、fib(4)が2回目に実行されるときに再帰呼び出しを行わず、メモ化した値を直接返すようにします。 その結果、forループと同じO(n)の計算量で実装することができます。 以下でメモ化する関数memorize()を追加しています。 index.js function memorize(fn) { const cache = {}; return function (...args) { if (cache[args]) { return cache[args]; } const result = fn.apply(this, args); cache[args] = result; return result; }; } function fib(n) { if (n < 2) { return n; } return fib(n - 1) + fib(n - 2); } fib = memorize(fib); これをテストしてみると、n=15で1msしかかかっていません。 ✓ Fib function is defined ✓ calculates correct fib value for 1 ✓ calculates correct fib value for 2 ✓ calculates correct fib value for 3 (1 ms) ✓ calculates correct fib value for 4 ✓ calculates correct fib value for 15 (1 ms) 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【アルゴリズム】JavaScriptでキュー/スタック問題を解く

はじめに Qiitaの競技プログラミング研究月間ということで、アルゴリズムの記事を書いています。 今回はキュー問題とスタック問題をまとめました。 JavaScriptでアルゴリズムの勉強をされている方の参考になれば幸いです。 記事を順次まとめていきますので、その他の記事についてはマイページからご覧ください。 キュー キューのデータ構造をもつクラスを作成してください。 データ追加、削除、削除データの参照のために、add(), remove(), peek()メソッドを作成してください。 解答 キューとはFIFO(First-In-First-Out)の特徴をもつデータ構造の一つです。 最初に並んだ人から食べられるといった、ラーメン屋の行列でよく例えられるやつです。 JSでキューを実現するには、配列の最初に要素を追加するunshift()や、配列から最後の要素を取り除くpop()といったメソッドを使います。 const q = new Queueで空のキューのインスタンスを作成し、キューにデータを追加する際にはq.add(~)、データを削除する際にはq.remove()のようにします。 index.js class Queue { constructor() { this.data = []; } add(record) { this.data.unshift(record); } remove() { return this.data.pop(); } peek() { return this.data[this.data.length - 1]; } } 複数キューの織り交ぜ(Weave) 2つのキューを引数に与えて、各データを織り交ぜた新しいキューを作成してください。 キューの配列データにアクセスせず、add(), remove(), peek()メソッドのみを使用してください。 const queueOne = new Queue(); queueOne.add(1); queueOne.add(2); const queueTwo = new Queue(); queueTwo.add('Hi'); queueTwo.add('There'); const q = weave(queueOne, queueTwo); q.remove() // 1 q.remove() // 'Hi' q.remove() // 2 q.remove() // 'There' 解答 まず、前項で作成したクラスQueueを関数作成前に読み込んでおきます。 空のキューのインスタンスconst q = new Queue()を用意し、ここに2つのキューから取り出したデータを交互に入れていきます。 while (sourceOne.peek() || sourceTwo.peek())で各キューのデータがなくなるまでループをまわし、データがあればq.add(sourceOne.remove())のように取り出したデータをqに入れるようにします。 index.js const Queue = require('./queue'); function weave(sourceOne, sourceTwo) { const q = new Queue(); while (sourceOne.peek() || sourceTwo.peek()) { if (sourceOne.peek()) { q.add(sourceOne.remove()); } if (sourceTwo.peek()) { q.add(sourceTwo.remove()); } } return q; } スタック スタックのデータ構造をもつクラスを作成してください。 データ追加、削除、削除データの参照のために、push(), pop(), peek()メソッドを作成してください。 解答 スタックとはLIFO(Last-In-First-Out)の特徴をもつデータ構造の一つです。 積み上がった本の山に例えられるやつですね。 JSでスタックを実現する際、pop()とpeek()メソッドはキューと同様ですが、要素追加にはpush()メソッドを使用します。 index.js class Stack { constructor() { this.data = []; } push(record) { this.data.push(record); } pop() { return this.data.pop(); } peek() { return this.data[this.data.length - 1]; } } スタックからキューの作成 2つのスタックを使用してキューを作成してください。 解答 2つのスタック(LIFO)を使うとキュー(FIFO)を実現することができます。 例えば、remove()の中では、1つ目のスタックthis.firstから取り出したデータを2つ目のスタックthis.secondに入れていき、this.secondの最後のデータをconst record = this.second.pop()で取り除いています。 index.js const Stack = require('./stack'); class Queue { constructor() { this.first = new Stack(); this.second = new Stack(); } add(record) { this.first.push(record); } remove() { while (this.first.peek()) { this.second.push(this.first.pop()); } const record = this.second.pop(); return record; } peek() { while (this.first.peek()) { this.second.push(this.first.pop()); } const record = this.second.peek(); return record; } } 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript 電卓 オブジェクト指向

ES6のクラス構文でオブジェクト指向な電卓を実装 JavaScriptやオブジェクト指向の初心者のため、不備やより良い記法があればご指摘いただけると幸いです コードの全容は以下 https://github.com/dstnk0208/CalclatorOOP/ JavaScriptのクラス構文について ES6から導入 実態はprototypeベースのシンタックスシュガー 以下の記事が大変参考になる https://www.hypertextcandy.com/object-oriented-javascript 仕様概要 四則演算ができる 演算は入力順に行う 1 + 2 * 3 = 9とする (7ではない、いわゆる逆ポーランド記法には非対応) 小数点は非サポート(入力や演算結果で出力はされるが、実装では表示のコントロールなどはしない) クラス設計 データ構造にはスタックを採用。Arrayオブジェクトを流用でき、逆ポーランド記法でも使われてるため 0除算のエラーなどにカスタムエラークラスを実装 カスタムエラーの実装には以下を参考した - https://qiita.com/necojackarc/items/c77cf3b5368b9d33601b 実装 Stackクラス class Stack { constructor() { this._a = []; } push(a) { try { // NaN or undefinedでカスタムエラーをスロー if ((!(a === a)) || (a === void 0)) { throw new InvalidInputException(); } this._a.push(a); } catch (e) { if (e instanceof InvalidInputException) { console.log(e); } else { console.log('other error'); } } } Calcクラス class Calc { //...中略... _div(v1, v2) { // 0除算でカスタムエラーをスロー if (v2 == 0) { throw new ZeroDevideException("Devided zero"); } stack.push(v1 / v2); } } ZeroDiviceExceptionクラス class ZeroDevideException extends Error { constructor(...params) { super(...params); Object.defineProperty(this, 'name', { value: this.constructor.name, }); if (Error.captureStackTrace) { Error.captureStackTrace(this, ZeroDevideException); } } } カスタムエラーはErrorをextendsするだけでは、カスタムエラーのErrorTypeがスタックトレースから欠落するため、constructorのnameプロパティを上書きする constructorのnameプロパティを上書きしなかった場合 Error at Calc._div (main.js:62) at Calc.calc (main.js:46) at onPushEqual (main.js:153) at HTMLButtonElement.onclick (index.html:32) 上書きした場合 ZeroDevideException at Calc._div (.././main.js:61:23) at Calc.calc (.././main.js:46:18) at onPushEqual (.././main.js:154:10) at HTMLButtonElement.onclick (.././index.html:32:45) まとめ ES6のクラス構文を使ってオブジェクト指向な電卓を実装した カスタムエラークラスも実装してみた 参考 https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes https://www.hypertextcandy.com/object-oriented-javascript https://qiita.com/necojackarc/items/c77cf3b5368b9d33601b
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript 日付ありオブジェクトの配列をソートする

概要 DynamoDB で日付を含むデータを取得し、新しい順で並び変えたときの備忘録です。 ソースコード var Items = [ { userId: "0001", createdAt: "2021-06-03T06:10:18.782Z" }, { userId: "0002", createdAt: "2021-06-03T07:06:47.336Z" } ]; Items.sort((a,b) => new Date(b.createdAt) - new Date(a.createdAt)); console.log(Items); 結果 [ { userId: '0002', createdAt: '2021-06-03T07:06:47.336Z' }, { userId: '0001', createdAt: '2021-06-03T06:10:18.782Z' } ]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

便利ページ:JavascriptでHTML5をサクッと試す

便利ページ:Javascriptでちょっとした便利な機能を作ってみた のシリーズです。 HTML5になって久しいですが、スマホの普及もあって、たくさんの機能がブラウザだけで実現できるようになっています。 そこで、以下の機能を、ブラウザ上からサクッと使えるようにしました。(HTML5の範疇ではないものもありますが。。。) ・Audio ・Video ・Image ・GeoLocation ・加速度センサ ・端末オリエンテーション ・バッテリー ・WebUsb ・WebBluetooth ・バイブレーション ・音声発話 ・音声認識 ・WebCam録画 ソースコードは以下のGitHubに上げてあります。 ブラウザから体験したい場合は以下にアクセスしてください。右上からHTML5を選択してください。  https://poruruba.github.io/utilities/ 主に、以下のファイルを追加しました。 js\comp\comp_html5.js Audio 音声ファイルを選択して、再生します。 audio_read: function (files) { if (files.length <= 0){ this.audio_type = ""; return; } var file = files[0]; this.audio_type = file.type; var reader = new FileReader(); reader.onload = () => { var audio = document.querySelector("#html5_audio_player"); audio.src = reader.result; audio.play(); }; reader.readAsDataURL(file); }, Video ビデオファイルを指定して、再生します。 video_read: function (files) { if (files.length <= 0){ this.video_type = ""; return; } var file = files[0]; this.video_type = file.type; var reader = new FileReader(); reader.onload = () => { var video = document.querySelector("#html5_video_player"); video.src = reader.result; video.play(); }; reader.readAsDataURL(file); }, Image 画像ファイルを指定して表示します。(HTML5ではないですが。。。) image_read: function (files) { if (files.length <= 0) { this.image_type = ""; this.image_src = ""; return; } var file = files[0]; this.image_type = file.type; var reader = new FileReader(); reader.onload = () => { this.image_src = reader.result; }; reader.readAsDataURL(file); }, GeoLocation いわゆるGPSを取得して表示します。 location_get: function(){ var options = { enableHightAccuracy: true, maximumAge: 0, timeout: 120000 } navigator.geolocation.getCurrentPosition((position) =>{ this.location = position.coords; }, (error) =>{ console.error(error); alert(error); }, options); }, 加速度センサ 端末に搭載されている加速度センサの測定値を表示します。主にスマホ用です。 motion_start: function () { if (!this.motion_running) { this.motion_running = true; window.addEventListener("devicemotion", this.motion_handler); } else { window.removeEventListener("devicemotion", this.motion_handler); this.motion_running = false; } }, motion_handler: function(event){ if (event.accelerationIncludingGravity.x === null ){ window.removeEventListener("devicemotion", this.motion_handler); this.motion_running = false; this.motion_supported = false; return; } this.accelerationIncludingGravity = event.accelerationIncludingGravity; this.motion_supported = true; }, 端末オリエンテーション 端末の向きを表示します。 orientation_start: function () { if (!this.orientation_running) { this.orientation_running = true; window.addEventListener("deviceorientation", this.orientation_handler); } else { window.removeEventListener("deviceorientation", this.orientation_handler); this.orientation_running = false; } }, orientation_handler: function (event) { if (event.alpha === null) { window.removeEventListener("deviceorientation", this.orientation_handler); this.orientation_running = false; this.orientation_supported = false; return; } this.orientation = event; this.orientation_supported = true; }, バッテリ バッテリの残容量等を表示します。 battery_refresh: function(){ navigator.getBattery() .then(battery => { this.battery = battery; }); } WebUSB WebUSBで、PCに接続されているUSBデバイスを表示します。 usb_request: async function () { try{ var device = await navigator.usb.requestDevice({ filters: [] }); this.usbdevice = device; }catch(error){ alert(error); } }, WebBluetooth WebBluetoothで、PCの周りにあるBLEデバイスを表示します。 ble_request: async function () { try{ var device = await navigator.bluetooth.requestDevice({ acceptAllDevices: [] }); this.bledevice = device; } catch (error) { alert(error); } }, バイブレーション 端末のバイブレーションを振動させます。主にスマホ向けです。 vibration_start: function(){ navigator.vibrate(this.vibration_duration); }, 音声発話 入力した文章を音声でスピーカから発話します。 speech_start: async function(){ var result = await new Promise((resolve, reject) => { var utter = new window.SpeechSynthesisUtterance(); utter.volume = this.speech_volume / 100; utter.rate = this.speech_rate / 100; utter.pitch = this.speech_pitch / 100; utter.text = this.speech_text; utter.lang = "ja-JP"; var ok = false; var ng = false; utter.onend = function () { console.log('Event(Utter) : onend'); if (!ok && !ng) { ok = true; resolve(); } }; utter.onstart = function () { console.log('Event(Utter) : onstart'); }; utter.onerror = function (error) { console.log('Event(Utter) : onerror'); if (!ok && !ng) { ng = true; reject(error); } }; window.speechSynthesis.cancel(); return window.speechSynthesis.speak(utter); }) .catch(error => { console.log(error); }); 音声認識 マイクから録音した音声から文章を認識します。 speech_recognition: async function(){ this.speech_recognized = await new Promise((resolve, reject) => { var recognition = new webkitSpeechRecognition(); recognition.lang = "ja-JP"; recognition.continuous = false; var match = false; var error = false; recognition.onresult = function (e) { console.log('Event(Recog) : onresult'); if (!match && !error) { match = true; var text = ''; for (var i = 0; i < e.results.length; ++i) { text += e.results[i][0].transcript; } resolve(text); } }; recognition.onend = function () { console.log('Event(Recog) : onend'); if (!match && !error) { match = true; reject(error); } recognition.stop(); }; recognition.onnomatch = function () { console.log('Event(Recog) : onnomatch'); if (!match && !error) { error = true; reject('nomatch'); } }; recognition.onerror = function (e) { console.log('Event(Recog) : onerror : ' + JSON.stringify(e)); if (!match && !error) { error = true; reject('onerror'); } }; recognition.start(); }) .catch(error => { console.log(error); }); }, WebCam録画 PCに接続されたWebCamやスマホにあるカメラで動画を録画し、ファイルに出力します。 一般的にwebm形式で保存されるようです。 record_prepare: function(){ this.record_dispose(); this.record_previewing = true; var resolution = this.record_resolution_list[this.record_resolution]; navigator.mediaDevices.getUserMedia({ video: { facingMode: this.record_facing, width: resolution.width, height: resolution.height }, audio: true }) .then((stream) => { g_stream = stream; this.record_preview.srcObject = stream; }) .catch(error =>{ this.record_previewing = false; alert(error); }); }, record_dispose: function(){ if( g_recorder ){ this.record_recording = false; g_recorder.stop(); g_recorder = null; } this.record_preview.pause(); this.record_preview.srcObject = null; g_stream = null; this.record_previewing = false; }, record_start: function(){ if (g_recorder) { g_recorder.stop(); g_recorder = null; } this.record_recording = true; this.record_chunks = []; g_recorder = new MediaRecorder(g_stream); g_recorder.ondataavailable = this.record_onavailable; g_recorder.onstop = this.record_onstop; g_recorder.start(); }, record_stop: function(){ console.log('stop'); g_recorder.stop(); }, record_onavailable: function (e) { console.log('ondataavailable'); this.record_chunks.push(e.data); }, record_onstop: function () { if( !this.record_recording ) return; console.log('onstop'); var blob = new Blob(this.record_chunks, { type: g_recorder.mimeType }); this.chunks = []; var mimeType = g_recorder.mimeType; g_recorder = null; this.record_recording = false; var url = window.URL.createObjectURL(blob); var a = document.createElement("a"); a.href = url; a.target = '_blank'; if (mimeType.startsWith('video/x-matroska') ) a.download = "record.webm"; else a.download = "record.bin"; a.click(); }, 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Next.js】Next.js+tailwind cssでシンプルなグルメ店検索アプリを作ってみた!

作ったもの 作ったアプリのタイトルは、東京グルメ店検索です。 実際のサービス ソース なお店舗情報の取得には、ホットペッパーWebサービスのグルメサーチAPIを利用させて頂きました。 この記事で学べること Next.jsによるシンプルなアプリの構築手順 tailwind cssの基本的な使い方 外部APIの叩き方 検索機能の実装 もっと読む機能の実装 学べないこと Type Script Next.jsの高度な使い方 開発環境 os: macOS Big Sur version 11.2 React.js: version 17.0 Next.js: version 10.2 tailwind css: version 2.1 開発の流れ この記事では、以下のような流れで開発を進めます。 APIキーの取得 Next.jsのセットアップ及び環境構築 実装 Vercelへデプロイ 1. APIキーの取得 APIキーの取得手順 このアプリでは、HOT PEPPERグルメのAPIを利用するため、事前にユーザ登録が必要になります。 以下の手順に従って、登録を行って下さい。 リクルートのWEBサービスサイトにアクセスし、新規登録ボタンをクリックします。 メールアドレスを入力し、送信ボタンをクリックします。 リクルートWebサービスからメールが届くので内容を確認し、承認する為のURLをクリックします。 承認の確認がとれると、登録したメールアドレス宛にAPIキーが送信されます。 最後にメールを受信し、リクルートWebサービスから発行されたAPIキーを確認して下さい。 APIのテスト 今回は、リクルートWebサービスのAPIの一つ、グルメサーチAPIを利用します。 リファレンスによるとグルメサーチAPIのエンドポイントは以下となります。 http://webservice.recruit.co.jp/hotpepper/gourmet/v1/ ブラウザに下記のURLを入力しページを開いてみて下さい。 https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key={取得したあなたのAPIキー}&keyword={好きなキーワード} このようなデータが表示されていれば、データの取得は成功です。 またグルメサーチAPIは、各種パラメータを指定することで検索対象を指定することができます。 例えばクエリにlarge_areaにZ011を含めることで、検索対象を東京に指定することができます。 下記URLを入力し、再度ブラウザで実行してみて下さい。 https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key={取得したあなたのAPIキー}&large_area=Z011&keyword={好きなキーワード} 結果が変更されました。 グルメサーチAPIは、他にも様々なパラメータを指定できます。 詳細については、公式リファレンスにて確認して下さい。 2. Next.jsのセットアップ及び環境構築 Next.jsのセットアップ ターミナルを立ち上げ、下記のコマンドを実行しNext.jsのプロジェクトを作成します。 プロジェクト名は、お好きなものを指定して下さい。 $ npx create-next-app プロジェクトが作成されたら下記のコマンドで開発者サーバを起動します。 ブラウザでhttp://localhost:3000を開き、作成したプロジェクトにアクセスできるか確認して下さい。 $ cd 作成したプロジェクト名 $ npm run dev 環境構築 API KEYの設定 先程の取得したAPI KEYの設定をプロジェクトに追加します。 .envファイルを作成し、API KEYの値を追加します。 $ touch .env .env API_KEY={あなたのAPI_KEYを入力} 設定したAPI_KEYをコード上で利用する際は、process.env.API_KEYと記述します。 以上でAPI KEYの設定は終了です。 tailwind cssのインストール このアプリでは、cssスタイリングにtailwind cssを利用します。 下記のコマンドを実行し、next.jsにtailwind cssを追加してください。 $ npm install -D tailwindcss@latest postcss@latest autoprefixer@latest $ npx tailwindcss init -p またstyles/globals.cssファイルに下記の記述を追加します。 styles/globals.css @tailwind base; @tailwind components; @tailwind utilities; tailwind cssには、公式の英語版の他に日本語版のドキュメントも用意されています。参照下さい。 node-fetchのインストール このアプリでは、Next.jsのAPIルートでfetch関数を使用するため、node-fetchを追加インストールします。 $ npm install -D node-fetch 以上で開発環境の構築は完了です。 3. 実装 実装の流れ 実装は、以下のような流れで進めます。 a. APIデータの取得 b. 取得データの一覧表示 c. スタイリング d. もっと読む機能の追加 e. 検索機能の追加 APIデータの取得 HOT PEPPER APIのレスポンスは、デフォルトではxml形式で返ってきます。 (私は当初、レスポンスがxml形式で返ってくることに気づかず、データが表示できないため???になっていました。) javascriptではそのままの形式では扱いづらいので、クエリにfomat=jsonを追加しjson形式を指定します。 pages/index.js const defaultEndpoint = 'https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?format=json' またHOT PEPPER APIは、ApiKeyの他に1つ以上の条件を指定しないと下記の様なエラーを返す仕様になっています。 (こういったエラーも初心者には気づきにくいので、初見のAPIを確認するときはコード上ではなく、まずブラウザを使ってレスポンスを確認することをオススメします。) このままだとエラーが返ってきてしまうため、データが取得できません。 そのためデフォルトの検索条件にエリアを東京large_area=Z011と追加することにしました。 pages/index.js const defaultEndpoint = `https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?format=json&large_area=Z011` 初期のエンドポイントは、ここにAPI KEYの指定を追加しています。 これでデータ取得が可能な形になりました。 pages/index.js const defaultEndpoint = `https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key=${process.env.API_KEY}&format=json&large_area=Z011` 初期のエンドポイントが決まったので、 次にコード上から取得したデータをブラウザのコンソール画面に値を表示し確認してみます。 以下のコードでは、サーバサイドで実行した取得結果dataをpropsをつかってHomeコンポーネントに渡し、ブラウザのコンソール画面に表示しています。 pages/index.js const defaultEndpoint = `https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key=${process.env.API_KEY}&format=json&large_area=Z011` export async function getServerSideProps() { const res = await fetch(defaultEndpoint) const data = await res.json() return { props: { data, }, } } export default function Home({ data }) { // コンソール画面に表示 console.log(data.results) } うまく表示されたでしょうか? コンソール画面表示でdataではなくdata.resultsを指定しているのは、APIのレスポンスフィールドのトップフィールドにresultsが指定されていためです。詳しくは、公式リファレンスを参照下さい。 取得したデータの一覧表示 次に取得した店舗データをmap関数を使って、画面に一覧表示します。 まず、pages/index.jsのHome関数の中身を削除し、画面表示をまっさらな状態にします。 pages/index.js export default function Home({ data }) { return ( <> <> ) } 次に、ここに一覧表示のコードを記述します。 リストに識別性を与えるため、それぞれの項目にkeyを設定します。 ここではkeyにindex番号を付与しました。 (当初、私はリストのkeyに店舗IDshop.idを割り当てていましたが、実際に実行してみると検索結果のレスポンスには同一店舗が複数含まれており、IDが重複していました。そのため、コンソール画面はwarnningで溢れました。最終的にリストのkeyをindex番号に変更し対処しました。) pages/index.js # 先頭行に追記 import Head from 'next/head' import Link from 'next/link' 〜〜〜 export default function Home({ data }) { return ( <> <Head> <title>東京グルメ店検索</title> </Head> <ul> {data.results.shop.map((item, index) => { return ( <li key={index}> <Link href={item.urls.pc}> <a> <div > <div > <div> <img src={item.photo.mobile.s} alt={item.name} /> </div> </div> <div> <div> {item.name}</div> <div> <div> <span> {item.genre.name} </span> <span>{item.catch}</span> </div> <p> {item.access}</p> </div> </div> </div> </a> </Link> </li> ) })} </ul> </> ) } まだスタイルがあたっていないので見づらいかもしれませんが、これで取得したデータが画面に表示されました。 またLinkを追加しているので、店舗の情報をクリックすると、各店舗サイトに画面が遷移するはずです。 スタイルを整える 続いて、tailwind cssを使って、画面表示を整えていきます。 pages/index.jsにインラインでスタイルを書き加えていきます。 pages/index.js export default function Home({ data }) { return ( <> <Head> <title>東京グルメ店検索</title> </Head> <div className="max-w-3xl font-mono bg-gray-100 mx-auto"> <ul className="mx-4"> {data.results.shop.map((item, index) => { return ( <li key={index} className="my-4 bg-white rounded border-red-500 border-2" > <Link href={item.urls.pc}> <a> <div className="grid grid-cols-10"> <div className="col-span-2 self-center"> <div> <img src={item.photo.mobile.s} alt={item.name} /> </div> </div> <div className="ml-3 col-span-8"> <div className="text-lg mt-2 mr-2"> {item.name}</div> <div className="text-xs mt-2 mr-2 pb-2"> <div className="text-xs"> <span className="font-medium"> {item.genre.name} </span> <span className="ml-4">{item.catch}</span> </div> <p className="mt-1"> {item.access}</p> </div> </div> </div> </a> </Link> </li> ) })} </ul> </div> </> ) } スタイルがあたることで、徐々にそれらしい画面になってきたのではないでしょうか。 もっと読む機能の追加 このままだと店舗データが規定値の10件までしか表示できないため、画面の下部にもっと読むボタンを追加し、追加データの表示ができるようにしたいと思います。 公式APIのドキュメントのレスポンスフィールドを確認すると検索結果の開始位置を示すresults_startと検索結果の件数results_returnedを使うと、もっと読む機能を実装できそうです。 ただ、このままクライアントサイドから外部APIであるリクルートWEBサービスにリクエストを送るとCORSエラーとなるため、今回はこのエラーを回避するためNext.jsのAPIルートを経由することにしました。 CORSについては、こちらの記事を参照下さい。 pages/index.js // 冒頭に追記 import { useState, useEffect } from 'react' export default function Home({ data }) { const { results_available = 0, results_start = 1, shop: defaultShops = [], } = data.results //取得した店舗データを格納 const [shop, updateShops] = useState(defaultShops) //取得したページデータを格納 const [page, updatePage] = useState({ results_available: results_available, results_start: results_start, }) // 開始位置の変更を監視 useEffect(() => { if (page.results_start === 1) return const params = { start: page.results_start, keyword: keyword } const query = new URLSearchParams(params) const request = async () => { const res = await fetch(`/api/search?${query}`) const data = await res.json() const nextData = data.results updatePage({ results_available: nextData.results_available, results_start: nextData.results_start, }) if (nextData.results_start === 1) { updateShops(nextData.shop) return } updateShops((prev) => { return [...prev, ...nextData.shop] }) } request() }, [page.results_start]) // もっと読むボタンを押したときの処理 const handlerOnClickReadMore = () => { if (page.results_returned <= page.results_start) return updatePage((prev) => { return { ...prev, results_start: prev.results_start + 1, } }) } return ( <> 〜〜〜 // もっと読むボタンを画面下部に追加 </ul> {page.results_returned <= page.results_start ? ( <div></div> ) : ( <div className="text-center pt-4 pb-8"> <button className="bg-red-500 rounded text-white tracking-wider font-medium hover:opacity-75 py-2 px-6 " onClick={handlerOnClickReadMore} > もっと読む </button> </div> )} </div> </> ) } pages/api/search.js import fetch from 'node-fetch' const defaultEndpoint = `https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key=${process.env.API_KEY}&format=json&large_area=Z011` export default async (req, res) => { let url = defaultEndpoint if (typeof req.query.start !== undefined) { url = `${url}&start=${req.query.start}` } url = encodeURI(url) const result = await fetch(url) res.json(result.body) } 処理の流れは、以下の通りです。 もっと読むボタンを押下すると関数handlerOnClickReadMoreが呼び出されます。 関数handlerOnClickReadMoreでは、現在の読み込み位置を示すresults_startに1を加えます。 useEffectがresults_startの変更を検知すると、追加のデータ取得処理が行われます。 追加データの取得先は、APIルート /api/search.jsとなります。 クエリパラメータには、先程のresults_start+1の値を加えます。 /api/search.jsは、与えられたパラメータに応じたレスポンスを返します。 再度useEffect内の処理に戻り、search.jsから返ってきたレスポンスを店舗データ配列に格納します。 画面が再描画され、追加分の店舗データが表示されます。 これで、もっと読むボタンを押す度に店舗データが追加表示されるようになりました。 検索機能の追加 次に画面上部に検索フォームを追加し、検索機能を実装します。 検索機能の処理は、渡すクエリパラメータがstart→keywordに変わっただけで、処理の流れは変わりません。 pages/index.js // キーワードを格納 const [keyword, setKeyword] = useState('') // キーワードの変更を監視 useEffect(() => { if (keyword === '') return const params = { keyword: keyword } const query = new URLSearchParams(params) // リクエスト、レスポンスの取得 const request = async () => { const res = await fetch(`/api/search?${query}`) const data = await res.json() const nextData = data.results updatePage({ results_available: nextData.results_available, results_start: nextData.results_start, }) updateShops(nextData.shop) } request() }, [keyword]) // 検索ボタン押下時の処理 const handlerOnSubmitSearch = (e) => { e.preventDefault() const { currentTarget = {} } = e const fields = Array.from(currentTarget?.elements) const fieldQuery = fields.find((field) => field.name === 'query') // keywordをセット const value = fieldQuery.value || '' setKeyword(value) } return ( <> <Head> <title>東京グルメ店検索</title> </Head> <div className="max-w-3xl font-mono bg-gray-100 mx-auto"> <div> <div className="text-2xl py-6 text-center"> <h2 className="font-medium tracking-wider ">東京グルメ店検索</h2> </div> <div className=""> <form onSubmit={handlerOnSubmitSearch} className="text-center"> <input type="search" name="query" className="rounded py-2 px-4 text-left border-red-500" placeholder="キーワードを入力して下さい" /> <button className="ml-2 text-white bg-red-500 rounded py-2 px-6 hover:opacity-75"> Search </button> </form> <div className="text-sm pt-2 text-gray-600 text-center"> <span>{page.results_available}</span> <span>件</span> </div> </div> </div> 〜〜〜 pages/api/search.js export default async (req, res) => { let url = defaultEndpoint if (typeof req.query.keyword !== undefined) { url = `${url}&keyword=${req.query.keyword}` } 実装すると検索機能が有効になります。 4. Vercelへデプロイ Vercelは、ホスティングサービスを一つです。 Next.jsで作成したアプリは、Vercelを使うとはじめてでもとても簡単にデプロイできます。 Vercelのデプロイ手順は、しまぶーさんのNext.jsプロジェクトをVercelにデプロイする方法が動画でとてもわかりやすく説明されています。 Vercelへのデプロイについて更に詳しく知りたい方は、公式ドキュメントを参照下さい。 おわりに いかがだったでしょうか? この記事が、これからNext.jsを使ってなにかアプリを作ってみようと思っている方のお役に立てれば幸いです。 参考サイト Next.js tailwindcss Doc HOT PEPPER グルメ API リファレンス Data Fetching in Next.js - Rick and Morty REST API Tutorial
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

stack machineを愛でる。 その5

概要 stack machineが、好きだ。 俺仕様アセンブラの処理系書いてみた。 サンプルコード function _arrayBufferToBase64(buffer) { var binary = ''; var bytes = new Uint8Array(buffer); var len = bytes.byteLength; for (var i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary); } var swf21 = [ 0x46, 0x57, 0x53, 0x06, 0x2F, 0x02, 0x00, 0x00, 0x68, 0x00, 0x1F, 0x40, 0x00, 0x05, 0xDC, 0x00, 0x00, 0x0C, 0x01, 0x00, 0x43, 0x02, 0xFF, 0xFF, 0xFF, 0x3F, 0x03, 0xBD, 0x01, 0x00, 0x00, 0x88, 0xA9, 0x01, 0x03, 0x00, 0x78, 0x00, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x00, 0x30, 0x31, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x00, 0x96, 0x04, 0x00, 0x08, 0x00, 0x08, 0x01, 0x1D, 0x96, 0x04, 0x00, 0x08, 0x00, 0x08, 0x02, 0x1D, 0x00, 0x3F, 0x0C, 0x19, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0x01, 0x10, 0x54, 0x69, 0x6D, 0x65, 0x73, 0x20, 0x4E, 0x65, 0x77, 0x20, 0x52, 0x6F, 0x6D, 0x61, 0x6E, 0x00, 0x00, 0x00, 0x02, 0x00, 0x7F, 0x09, 0x1F, 0x00, 0x00, 0x00, 0x02, 0x00, 0x6F, 0xF6, 0x1D, 0x0F, 0xFD, 0x85, 0x28, 0x00, 0x6D, 0x20, 0x01, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x78, 0x00, 0x89, 0x06, 0x06, 0x01, 0x00, 0x02, 0x00, 0x12, 0x8C, 0x6E, 0x00, 0x40, 0x00, 0x00, 0x00 ]; function tob(url) { var bin = atob(url); var buffer = new Uint8Array(bin.length); for (var i = 0; i < bin.length; i++) { buffer[i] = bin.charCodeAt(i); } var blob = new Blob([buffer.buffer], { type: 'application/x-shockwave-flash' }); var blob_url = window.URL.createObjectURL(blob); return blob_url; } function run() { var c1, c2, c3, a, i, j; var pc = 0; var la = new Array(20); var r = new Array(400); var src = document.getElementById('src0').value; var m = src.split("\n"); for (j = 0; j < m.length ; j++) { var cm = m[j].split(" "); if (cm[0] != "") { la[cm[0]] = pc; } switch (cm[1]) { case "push": r[pc] = 0x96; pc++; c1 = cm[2].indexOf("\"") + 1; c2 = cm[2].indexOf("\"", 1) - c1; a = cm[2].substr(c1, c2); c3 = c2 + 2; r[pc] = c3; pc++; r[pc] = 0x00; pc++; r[pc] = 0x00; pc++; for (i = 0; i < c2; i++) { r[pc] = a.charCodeAt(i); pc++; } r[pc] = 0x00; pc++; break; case "end": r[pc] = 0x00; pc++; break; case "+": r[pc] = 0x0A; pc++; break; case "-": r[pc] = 0x0B; pc++; break; case "*": r[pc] = 0x0C; pc++; break; case "/": r[pc] = 0x0D; pc++; break; case "=": r[pc] = 0x0E; pc++; break; case "&": r[pc] = 0x60; pc++; break; case "|": r[pc] = 0x61; pc++; break; case "^": r[pc] = 0x62; pc++; break; case "%": r[pc] = 0x3f; pc++; break; case ">": r[pc] = 0x0F; pc++; break; case "&&": r[pc] = 0x10; pc++; break; case "||": r[pc] = 0x11; pc++; break; case "!": r[pc] = 0x12; pc++; break; case "==": r[pc] = 0x13; pc++; break; case "pop": r[pc] = 0x17; pc++; break; case "get": r[pc] = 0x1C; pc++; break; case "++": r[pc] = 0x50; pc++; break; case "set": r[pc] = 0x1D; pc++; break; case "add": r[pc] = 0x21; pc++; break; case "if": r[pc] = 0x9D; pc++; r[pc] = 0x02; pc++; r[pc] = 0x00; pc++; if (la[cm[2]] > 1) { c3 = la[cm[2]] - pc + 254; r[pc] = c3; pc++; r[pc] = 0xFF; pc++; } else { r[pc] = cm[2]; pc++; r[pc] = 0x00; pc++; } break; case "jp": r[pc] = 0x99; pc++; r[pc] = 0x02; pc++; r[pc] = 0x00; pc++; if (la[cm[2]] > 1) { c3 = la[cm[2]] - pc + 254; r[pc] = c3; pc++; r[pc] = 0xFF; pc++; } else { r[pc] = cm[2]; pc++; r[pc] = 0x00; pc++; } break; } } for (var l in la) { for (i = 0; i < pc; i++) { if (r[i] == l) { r[i] = la[l] - i - 2; } } } for (i = 0; i < pc; i++) { swf21[i + 31] = r[i]; } var b = _arrayBufferToBase64(swf21); var h = '<embed src ="'; var l = '" type="application/x-shockwave-flash" width="300" height="300"></embed>'; var swf = h + tob(b) + l; var out = document.getElementById('out'); out.innerHTML = swf; } 成果物 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

stack machineを愛でる。 その4

概要 stack machineが、好きだ。 俺仕様アセンブラで、helloworld書いてみた。 サンプルコード push "x" push "Hello World!" set 説明 変数xに、テキストを代入すると、表示される仕様です。 変数に、テキストを代入するには、setを使います。 setは、スタックの二番目を変数として、一番目の値をセットしますので pushで、"x"と"Hello World!"をスタックに積んで、setを実行します。 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

stack machineを愛でる。 その3

概要 stack machineが、好きだ。 ruffleを使って、ボタンを押したら、swfを生成するを作る。 サンプルコード function run() { var b = _arrayBufferToBase64(swf21); var l = '" type="application/x-shockwave-flash" width="300" height="300"></embed>'; var swf = '<embed src ="' + tob(b) + l; document.write(swf); } 成果物 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js とは何なのか?なぜ学ぶ必要があるのか?

Node.jsってなんやねん?なんで知っとかんとあかんねん? という超初歩的な疑問に対して答えようと思うのじゃ。 元々はphpとSQLというウェブの作り方はもう時代遅れだから、既存のサイトを全てnodeとnon-SQLに置き換えたいというプロジェクトがあり、20年以上ウェブの現場から遠ざかっていた身としては、nodeっていうのはphpみたいなサーバー側の言語かなぁくらいに思っていたわけですが、調べると、サーバー側で動くJavascriptっていう言い方がよくされていて、さらに触れてみると、その言い方も間違いで、サーバー側でJavascriptを動かす為の実行環境と便利なモジュール群が正しい表現の仕方かなと。 元々javascriptって、ブラウザ上でしか動かないんで、サーバーサイドはphpとかpythonとか他の言語で書かないといけなかったわけです。 で、両方で動いたほうが便利やん!って思ったギークたちが両方で動くようにしてしまったわけです。 つまり、javascriptでOSの機能とかファイルシステムにもアクセスできてしまうわけです。phpで書いていたのを、javascriptの.jsで代替できるっていうことでもあります。 で、どうやって使うねん?と思うかも知れませんが、https://nodejs.orgからNode.jsをあなたのコンピューター(サーバー)にダウンロードするだけです。 で、それから、どうやって使うねん?基本的にコマンドラインからの操作か、コードエディタでサーバー側コードをJavascriptで書いて、サーバー側で実行です。クライアントサイドのjavascriptはブラウザで動作確認できるよね? nodeをインストールしたら、コマンドラインを立ち上げて、node とコマンドを打つだけで環境が立ち上がります。 ubuntu@ubuntu-aurora-r4:~$ node Welcome to Node.js v13.14.0. Type ".help" for more information. > で、普通にjavascriptのコードを打つとちゃんとコードが実行されているのが分かります。 ubuntu@ubuntu-aurora-r4:~$ node Welcome to Node.js v13.14.0. Type ".help" for more information. > > let x=11; let y=15; let sum=x+y; console.log(sum) 26 > const name='henoheno'; console.log(name); henoheno > ブラウザが無くても、OSの中(つまりサーバー内)でjavascriptが動いてるよね? つまりこれがnodeの力。 ブラウザからフォームとかを通してjavascriptで書かれたリクエストが来ると、サーバー側のjavascript(node)でそのリクエストを処理して、送り返すっていう感じ。 普通のjavascriptと何が違うんじゃい? 例えば、ブラウザ側で使われるjavascriptにあるような、Windowオブジェクトとか、Documentオブジェクトはありません。だってブラウザとか画面は関係ないんだから。 その代わりに、fsコマンドや、Processオブジェクトなど、ファイルの操作に使うようなオブジェクトやメソッドが存在します。 例えば、ブラウザ側ではAPIからデータを持ってくる場合、fetch()を使いますが、node側ではrequestモジュールを使ったり。 基本的に、元々のnodeに入ってるモジュールはグローバルと言って、ほんの少ししか入っていないので、ほぼ裸の状態です。色々便利なモジュールは、別途インストールして自分で服を着ていかないといけません。 モジュールのインストールや、操作はnpmっていうモジュールをインストールして行います。 こいつがいないと、node環境での作業は捗りません。まさにnode界のドラえもんです。 使い方は次のポストで書いていきます。 なんでnodeやねん!phpでもええんやないのか? nodeはNetflix、Facebook、PaypalやAmazonでも使われています。 勉強していくとわかるんだけど、どちらかというと今までみたいなメニューがずらっと並んでてリンクがあるようなサイトよりも、Web APPやAPI、ヘッドレス、シングルページのウェブに非常に向いてる感じ。 デプロイの仕方も全然違います。 nodeの特徴としてよく言われるのが、”ノンブロッキングI/O”というコンセプト。 javascriptはシングルスレッドで、1個づつしかコードを実行できないんだけど、ポイントは”待ち時間”。 料理に例えると分かりやすい。サラダとスープとステーキとデザートを作るとする。 スープから作るとすると、スープを煮込んでる間にサラダの準備とステーキの仕込みが出来るのが、”ノンブロッキング”。スープが仕上がるまで、鍋の前で待っていないといけないのが、”ブロッキング”。phpは元々これ。 この場合、ステーキが焼き上がるまで、デザートの準備も出来ないことになり、全てのコースが揃うまで膨大な時間が必要になる。 その点、ノンブロッキングは一度コードを実行させると、待ってる間に他のコードも複数実行させる事が出来る。 nodeはこの点が非常に優れており、結果的に多くのリクエストをすばやく効果的に捌けるため、超軽くて速いっていう事になる。 実際にサーバーの立て方、ディレクトリの構成、ページの作り方を少しづつアップしていく予定です。がんばれ、俺。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む