- 投稿日:2020-10-23T23:47:03+09:00
GraphQLとVueを使ってミニアプリを作る
はじめに
本記事ではVue Apolloを利用して、GraphQLとVueアプリを接続した後に、CRUD機能並びにサブスクリプションの実装を目指す内容となります。
大まかな概要としては、Apolloサーバーから取得したデータをVueを利用して表示・クライアント側からGraphQLを操作することがゴールとなります。
記事内でGraphQL等の基本的な説明は省略しております。
もし宜しければ、GraphQLの基礎の基礎、並びにApolloサーバーとサブスクリプションについて解説している前回の記事も合わせてご一読ください(Apolloサーバー部分は前回と同一のコードを利用します)Apolloサーバーを作る(前回の記事で既に作ってある方は省略)
先ずはApolloを使ってGraphQLのAPIサーバーを作ります!
プロジェクトを作った後に、npm経由でapollo-serverをインストールします。
$ mkdir Apollo-Server $ cd Apollo-Server $ mkdir resolver $ touch index.js db.js $ resolver/{Query.js,Mutation.js,Subscription.js} $ npm init $ npm install apollo-server --saveデータベース代わりのJavaScriptファイルを用意します。
db.jsconst posts = [{ id: '1', title: 'こころ', author: '夏目漱石' }, { id: '2', title: '舞姫', author: '森鴎外' }, { id: '3', title: '羅生門', author: '芥川龍之介' }] const db = { posts, } module.exports = db;Query,Mutation,Subscriptionに関する処理をまとめたファイルを順番に用意していきます。
Query.jsconst Query = { posts(parent, args, { db }, info) { //クエリを書いた時に引数が「ない」時 //模擬データベースの内容を全て表示 if (!args.query) { return db.posts //クエリを書いた時に引数が「ある」時 //引数とtitle or authorが一致したものだけを表示 }else{ return db.posts.filter((post) => { const isTitleMatch = post.title.toLowerCase().includes(args.query.toLowerCase()) const isAuthorMatch = post.author.toLowerCase().includes(args.query.toLowerCase()) return isTitleMatch || isAuthorMatch }) } } } module.exports = Queryクエリに関する処理をまとめたファイルです。
Mutation.jsconst Mutation = { createPost(parent, args, { db, pubsub }, info) { const postNumTotal = String(db.posts.length + 1) const post = { id: postNumTotal, ...args.data } //データベース更新 db.posts.push(post) //サブスクリプション着火 pubsub.publish('post', { post: { mutation: 'CREATED', data: post } }) return post }, updatePost(parent, args, { db, pubsub }, info) { const { id, data } = args const post = db.posts.find((post) => post.id === id) if (!post) { throw new Error('Post not found') } if (typeof data.title === 'string' && typeof data.author === 'string') { //データベース更新 post.title = data.title post.author = data.author console.log(post) //サブスクリプション着火 pubsub.publish('post', { post: { mutation: 'UPDATED', data: post } }) } return post }, deletePost(parent, args, { db, pubsub }, info) { const post = db.posts.find((post) => post.id === args.id) const postIndex = db.posts.findIndex((post) => post.id === args.id) if (postIndex === -1) { throw new Error('Post not found') } //データベース更新 db.posts.splice(postIndex, 1) //サブスクリプション着火 pubsub.publish('post', { post: { mutation: 'DELETED', data: post } }) return post }, } module.exports = Mutationミューテーションの処理内でデータベースの更新とサブスクリプションの着火をしています。
Subscription.jsconst Subscription = { post: { subscribe(parent, args, { pubsub }, info) { return pubsub.asyncIterator('post') } } } module.exports = Subscriptionサブスクリプションのファイルです。
pubsub.asyncIteratorでサブスクリプションのイベントを非同期でリッスンします。説明の関係で最後になりましたが、
スキーマの定義とサーバー起動のファイルになります。index.jsconst {ApolloServer,PubSub,gql} = require('apollo-server'); const db = require('./db') const Query = require('./resolver/Query') const Mutation = require('./resolver/Mutation') const Subscription = require('./resolver/Subscription') //スキーマ定義 const typeDefs = gql` type Query { posts(query: String): [Post!]! } type Mutation { createPost(data: CreatePostInput!): Post! deletePost(id: ID!): Post! updatePost(id: ID!, data: UpdatePostInput!): Post! } # サブスクリプション type Subscription { post: PostSubscriptionPayload! } input CreatePostInput { title: String! author: String! } input UpdatePostInput { title: String author: String! } type Post { id: ID! title: String! author: String! } ###################### # サブスクリプションで利用 ###################### # enum型でMutation.js内のサブスクリプション着火と連動 enum MutationType { CREATED UPDATED DELETED } # サブスクリプションのフィールド type PostSubscriptionPayload { mutation: MutationType! data: Post! } ` //PubSubのインスタンスを作成,サブスクリプションが利用可能に! const pubsub = new PubSub() const server = new ApolloServer({ typeDefs: typeDefs, resolvers: { Query, Mutation, Subscription, }, context: { db, pubsub } }) server.listen().then(({ url, subscriptionsUrl }) => { console.log(`? Server ready at ${url}`); console.log(`? Subscriptions ready at ${subscriptionsUrl}`); });サーバーを立てる際にスキーマやリゾルバ、PubSubなどを引数に指定しています。
指定した引数はクエリやミューテーションそしてサブスクリプション、それぞれの処理で利用をしています。準備が出来たらターミナルから起動させます。
$ node index.js ? Server ready at http://localhost:4000/ ? Subscriptions ready at ws://localhost:4000/graphqlこちらをGraphQLのエンドポイントとして利用します。
本記事ではサーバーが立ち上がっていないと、クライアント側からデータの表示や更新が出来ないので、必ずサーバーを立てることを忘れないようにしてください?Apolloクライアントの設定
Vueプロジェクトを作ろう
Vue CLIを使ってプロジェクトを作ります。
$ vue create apollo-client #2系を選択してVueプロジェクトの作成 $ cd apollo-client $ npm run serve # 起動プロジェクトの作成に成功していた場合、下記のような画面になります。
http://localhost:8080/Vueプロジェクト内の設定
続いてVueでGraphQLを扱うために、Vue Apolloをインストールしていきます。
本記事においては設定は全てデフォルトで進みます(色々聞かれますが「No」と答えてます)$ vue add apollo見た目をリッチにするために、Vuetifyもインストールします。
$ vue add vuetify以上にて、プロジェクトの設定は完了です。
Vue-ApolloとVuetifyの設定はmain.jsに記述があります。main.jsimport Vue from 'vue' import App from './App.vue' import { createProvider } from './vue-apollo' import vuetify from './plugins/vuetify'; Vue.config.productionTip = false new Vue({ apolloProvider: createProvider(), vuetify, render: h => h(App) }).$mount('#app')importしているvue-apollo.jsではエンドポイントやログイン/ログアウトなどの設定が出来ます。
クエリの実装
クエリとミューテーションの実装はこちらの記事を参考にさせて頂きました。ありがとうございます!
模擬データベース内のデータを「読み取り」するGraphQLのクエリを作成します。
$ mkdir src/graphql $ touch src/graphql/post-query.js前回までの記事でlocalhost内で立ち上げたIDEに書いていたクエリがこちらの部分に該当します。importしているgqlについてはこちらを参照ください。
post-query.jsimport gql from 'graphql-tag' export const ALL_POSTS = gql` query{ posts{ id title author } } `続いてApp.vue内を書き換えます(説明の省略のため、こちらのファイルに書いていますが、component化した方が良いです!)
「apollo」のオプション内で別ファイルで定義したクエリを呼び出し、
dataプロパティで定義したpostsオブジェクトに格納します。App.vue<template> <v-app> <v-main> <v-container> <v-row style="width: 550px;" > <!--ツールバー--> <v-toolbar color="grey lighten-1"> <v-toolbar-title>本棚</v-toolbar-title> <v-spacer></v-spacer> <v-btn color="primary" dark class="mb-1">新規追加</v-btn> </v-toolbar> <!--本棚の中身--> <div v-for="post in posts" :key="post.id" > <v-card class="mx-auto" width="550px" outlined > <v-list-item three-line> <v-list-item-content> <v-list-item-title class="headline mb-1"> {{ post.title}}/{{ post.author}} </v-list-item-title> <v-list-item-subtitle>From Apollo-Server</v-list-item-subtitle> </v-list-item-content> </v-list-item> </v-card> </div> </v-row> </v-container> </v-main> </v-app> </template> <script> //Query import {ALL_POSTS} from "./graphql/post-query" export default { name: "App", data: () => ({ //本棚の中身を定義 posts: [], }), apollo: { //本棚の中身 posts: { //クエリを書いている部分 query: ALL_POSTS, } }, methods: { } } </script>ここまで書き終えたらブラウザで確認しましょう。
$ npm run serveVueを利用して模擬データベースの値を表示することが出来ました!
ミューテーションの実装
続いてミューテーションの実装を行います。
こちらの章を終えると①書き込み、②更新、③削除 が出来るようになります。先ずはファイルを作りましょう。
$ touch src/graphql/post-mutation.jsミューテーションを書いていきます。
post-mutation.jsimport gql from 'graphql-tag' // POSTの新規追加 export const CREATE_POST = gql`mutation ($title: String!, $author: String!) { createPost(data: { title: $title, author: $author}) { id title author } }` // POSTの更新 export const UPDATE_POST = gql` mutation updatePost($id: ID!, $title: String!, $author: String!) { updatePost(id:$id,data: {title: $title, author: $author}) { id title author } } ` // // POSTの削除 export const DELETE_POST = gql` mutation deletePost($id: ID!) { deletePost(id:$id){ title author } } `App.vueのmethod内に①書き込み、②更新、③削除の関数を作成。
updateQueryメソッドで上記3つを実行することが出来ます。App.vue<template> <v-app> <v-main> <v-container> <!--入力フォーム--> <v-dialog v-model="dialog" max-width="500px"> <v-card> <v-container> <h2 v-if="isCreate">本棚に追加する</h2> <h2 v-if="!isCreate">本棚を更新する</h2> <v-form ref="form" v-model="valid" lazy-validation> <!--名前--> <v-text-field v-model="post.title" :rules="titleRules" :counter="20" label="タイトル" required ></v-text-field> <v-text-field v-model="post.author" :rules="authorRules" :counter="20" label="作者" required ></v-text-field> <!--追加ボタン--> <v-btn v-if="isCreate" :disabled="!valid" @click="createPost" > 追加 </v-btn> <!--更新ボタン--> <v-btn v-if="!isCreate" :disabled="!valid" @click="updatePost" > 更新 </v-btn> <v-btn @click="clear">クリア</v-btn> </v-form> </v-container> </v-card> </v-dialog> <v-row style="width: 550px;" > <!--ツールバー--> <v-toolbar color="grey lighten-1"> <v-toolbar-title>本棚</v-toolbar-title> <v-spacer></v-spacer> <v-btn color="primary" dark class="mb-1" @click="showDialogNew">新規追加</v-btn> </v-toolbar> <!--本棚の中身--> <div v-for="post in posts" :key="post.id" > <v-card class="mx-auto" width="550px" outlined > <v-list-item three-line> <v-list-item-content> <v-list-item-title class="headline mb-1"> {{ post.title}}/{{ post.author}} </v-list-item-title> <v-list-item-subtitle>From Apollo-Server</v-list-item-subtitle> </v-list-item-content> </v-list-item> <!-- 編集・削除ボタン --> <v-card-actions> <v-btn color="success" small @click="showDialogUpdate(post.id,post.title,post.author)" > <v-icon small> 編集する </v-icon> </v-btn> <v-btn color="error" small @click="deletePost(post.id,post.title)" > <v-icon small> 削除する </v-icon> </v-btn> </v-card-actions> </v-card> </div> </v-row> </v-container> </v-main> </v-app> </template> <script> //Query import {ALL_POSTS} from "./graphql/post-query" //Mutation import {CREATE_POST,UPDATE_POST,DELETE_POST} from "./graphql/post-mutation"; export default { name: "App", data: () => ({ //本棚の中身を定義 posts: [], // フォーム入力値 post: { id: '', title: '', author: '', }, // バリデーション valid: true, titleRules: [ v => !!v || 'タイトルは必須項目です', v => (v && v.length <= 20) || 'タイトルは20文字以内で入力してください' ], authorRules: [ v => !!v || '作者名は必須項目です', ], // ローディングの表示フラグ progress: false, // ダイアログの表示フラグ dialog: false, // 新規・更新のフラグ isCreate: true, }), apollo: { //本棚の中身 posts: { //クエリを書いている部分 query: ALL_POSTS, } }, methods: { // -------------------------------- // 新規作成 // -------------------------------- createPost: function () { if (this.$refs.form.validate()) { this.progress = true this.$apollo.mutate({ mutation: CREATE_POST, variables: { title: this.post.title, author: this.post.author, }, }) .then(() => { //UIの更新 this.$apollo.queries.posts.fetchMore({ updateQuery: (previousResult, {fetchMoreResult}) => { // console.log(previousResult) //変更前 // console.log(fetchMoreResult) //変更後 return { posts: fetchMoreResult.posts } } }) this.dialog = false this.progress = false }).catch((error) => { console.error(error) }) } } , // -------------------------------- // 更新 // -------------------------------- updatePost: function () { this.progress = true this.$apollo.mutate({ mutation: UPDATE_POST, variables: { id: this.post.id, title: this.post.title, author: this.post.author, } }).then(() => { this.$apollo.queries.posts.fetchMore({ updateQuery: (previousResult, {fetchMoreResult}) => { // console.log(previousResult) //変更前 // console.log(fetchMoreResult) //変更後 return { posts: fetchMoreResult.posts } } }) this.dialog = false this.progress = false }).catch((error) => { console.error(error) }) }, // -------------------------------- // 削除 // -------------------------------- deletePost: function (id,title) { console.log(id) console.log(title) if (!confirm(title + 'を削除してもよろしいですか?')) { return } this.progress = true this.$apollo.mutate({ mutation: DELETE_POST, variables: { id: id } }).then(() => { this.$apollo.queries.posts.fetchMore({ updateQuery: (previousResult, {fetchMoreResult}) => { // console.log(previousResult) //変更前 // console.log(fetchMoreResult) //変更後 return { posts: fetchMoreResult.posts } } }) this.progress = false }).catch((error) => { console.error(error) }) }, // -------------------------------- // フォームのクリア // -------------------------------- clear: function () { this.$refs.form.reset() }, // -------------------------------- // 新規追加ダイアログの表示 // -------------------------------- showDialogNew: function () { // this.clear() this.isCreate = true this.dialog = true }, // -------------------------------- // 更新ダイアログの表示 // -------------------------------- showDialogUpdate: function (id, title, author) { this.post.id = id this.post.title = title this.post.author = author this.isCreate = false this.dialog = true }, } } </script>CRUD処理が出来るようになりました!!
サブスクリプションの実装
最後にサブスクリプションの実装を行います。
こちらの章を終えるとリアルタイムでの書き込みの共有が出来るようになります。
実際のアプリケーションでは通知機能を作る時などに使う部分となります。今回もファイルを作ります。
$ touch src/graphql/post-subscription.jsサブスクリプションを書きます。
post-subscription.jsimport gql from 'graphql-tag' // サブスクリプション export const SUBSCRIPTION_POST = gql` subscription { post{ mutation data{ id title author } } } `App.vueを変更します。
「apollo」のオプション内にサブスクリプションの処理を追加しており、既存の投稿と同一の投稿がなかった場合、新規作成した投稿を本棚に追加しています。App.vue<template> <v-app> <v-main> <v-container> <!--入力フォーム--> <v-dialog v-model="dialog" max-width="500px"> <v-card> <v-container> <h2 v-if="isCreate">本棚に追加する</h2> <h2 v-if="!isCreate">本棚を更新する</h2> <v-form ref="form" v-model="valid" lazy-validation> <!--名前--> <v-text-field v-model="post.title" :rules="titleRules" :counter="20" label="タイトル" required ></v-text-field> <v-text-field v-model="post.author" :rules="authorRules" :counter="20" label="作者" required ></v-text-field> <!--追加ボタン--> <v-btn v-if="isCreate" :disabled="!valid" @click="createPost" > 追加 </v-btn> <!--更新ボタン--> <v-btn v-if="!isCreate" :disabled="!valid" @click="updatePost" > 更新 </v-btn> <v-btn @click="clear">クリア</v-btn> </v-form> </v-container> </v-card> </v-dialog> <v-row style="width: 550px;" > <!--ツールバー--> <v-toolbar color="grey lighten-1"> <v-toolbar-title>本棚</v-toolbar-title> <v-spacer></v-spacer> <v-btn color="primary" dark class="mb-1" @click="showDialogNew">新規追加</v-btn> </v-toolbar> <!--本棚の中身--> <div v-for="post in posts" :key="post.id" > <v-card class="mx-auto" width="550px" outlined > <v-list-item three-line> <v-list-item-content> <v-list-item-title class="headline mb-1"> {{ post.title}}/{{ post.author}} </v-list-item-title> <v-list-item-subtitle>From Apollo-Server</v-list-item-subtitle> </v-list-item-content> </v-list-item> <!-- 編集・削除ボタン --> <v-card-actions> <v-btn color="success" small @click="showDialogUpdate(post.id,post.title,post.author)" > <v-icon small> 編集する </v-icon> </v-btn> <v-btn color="error" small @click="deletePost(post.id,post.title)" > <v-icon small> 削除する </v-icon> </v-btn> </v-card-actions> </v-card> </div> </v-row> </v-container> </v-main> </v-app> </template> <script> //Query import {ALL_POSTS} from "./graphql/post-query" //Mutation import {CREATE_POST,UPDATE_POST,DELETE_POST} from "./graphql/post-mutation"; //Subscription import {SUBSCRIPTION_POST} from "./graphql/post-subscription"; export default { name: "App", data: () => ({ //本棚の中身を定義 posts: [], // フォーム入力値 post: { id: '', title: '', author: '', }, // バリデーション valid: true, titleRules: [ v => !!v || 'タイトルは必須項目です', v => (v && v.length <= 20) || 'タイトルは20文字以内で入力してください' ], authorRules: [ v => !!v || '作者名は必須項目です', ], // ローディングの表示フラグ progress: false, // ダイアログの表示フラグ dialog: false, // 新規・更新のフラグ isCreate: true, }), apollo: { //本棚の中身 posts: { //クエリを書いている部分 query: ALL_POSTS, //サブスクリプション subscribeToMore: { document: SUBSCRIPTION_POST, updateQuery:(previousResult, { subscriptionData }) =>{ // console.log(previousResult) //前の投稿 // console.log(subscriptionData.data) //新規作成した投稿 // 既存の投稿と同一の投稿がなかった場合、新規作成した投稿を本棚に追加 if (previousResult.posts.find(post => post.id === subscriptionData.data.post.data.id)) { return previousResult }else{ return { posts: [ ...previousResult.posts, // Add the new data subscriptionData.data.post.data, ], } } } } } }, methods: { // -------------------------------- // 新規作成 // -------------------------------- createPost: function () { if (this.$refs.form.validate()) { this.progress = true this.$apollo.mutate({ mutation: CREATE_POST, variables: { title: this.post.title, author: this.post.author, }, }) .then(() => { //UIの更新 this.$apollo.queries.posts.fetchMore({ updateQuery: (previousResult, {fetchMoreResult}) => { // console.log(previousResult) //変更前 // console.log(fetchMoreResult) //変更後 return { posts: fetchMoreResult.posts } } }) this.dialog = false this.progress = false }).catch((error) => { console.error(error) }) } } , // -------------------------------- // 更新 // -------------------------------- updatePost: function () { this.progress = true this.$apollo.mutate({ mutation: UPDATE_POST, variables: { id: this.post.id, title: this.post.title, author: this.post.author, } }).then(() => { this.$apollo.queries.posts.fetchMore({ updateQuery: (previousResult, {fetchMoreResult}) => { // console.log(previousResult) //変更前 // console.log(fetchMoreResult) //変更後 return { posts: fetchMoreResult.posts } } }) this.dialog = false this.progress = false }).catch((error) => { console.error(error) }) }, // -------------------------------- // 削除 // -------------------------------- deletePost: function (id,title) { console.log(id) console.log(title) if (!confirm(title + 'を削除してもよろしいですか?')) { return } this.progress = true this.$apollo.mutate({ mutation: DELETE_POST, variables: { id: id } }).then(() => { this.$apollo.queries.posts.fetchMore({ updateQuery: (previousResult, {fetchMoreResult}) => { // console.log(previousResult) //変更前 // console.log(fetchMoreResult) //変更後 return { posts: fetchMoreResult.posts } } }) this.progress = false }).catch((error) => { console.error(error) }) }, // -------------------------------- // フォームのクリア // -------------------------------- clear: function () { this.$refs.form.reset() }, // -------------------------------- // 新規追加ダイアログの表示 // -------------------------------- showDialogNew: function () { // this.clear() this.isCreate = true this.dialog = true }, // -------------------------------- // 更新ダイアログの表示 // -------------------------------- showDialogUpdate: function (id, title, author) { this.post.id = id this.post.title = title this.post.author = author this.isCreate = false this.dialog = true }, } } </script>確認してみましょう。ブラウザのウィンドウを2つ用意します。
こちらも上手くいきました!!!
おわりに
以上、今回はGraphQLをVueで扱ってみました。
今回の内容を元に仕様に合わせてクエリやスキーマの数を増やすことで、発展的なアプリケーションを作ることが出来るかと思います。
次回はGraphQLとデータベースの接続について記事にしたいと思います!それでは、また?
- 投稿日:2020-10-23T23:40:08+09:00
HTMLを読み込んでからJSが読み込まれるようにする方法
はじめに
少し苦手意識のあるJSでエラーが出たので、忘れないために…
コードは合っているのに、エラーが出た
JSとHTMLは切っても切れない関係。
HTMLを読み込み、JSが読み込まれることで、JSがしっかりと動いてくれる。
そう、逆だと動かない。
JSを読み込み、HTMLを後から読み込むと、エラーが出る。考えればわかることだが、JSではgetElementByIdなどで、HTMLに記述されているid名を読み出す。
HTMLを読み込んでから、JSが読み込まれるように、次の記述をJSにする。window.addEventListener('load', () => { //処理をここに記述 });コードの意味は、「ページをloadしたら、イベントを発火させる」
基本これを忘れずに、最初に記述しておく。
- 投稿日:2020-10-23T22:14:33+09:00
Laravel8.9.0 + Vue.jsでaxiosを使ったときにthenとcatchの両方を通るときの対処法
- 投稿日:2020-10-23T21:19:27+09:00
5.TMDB リスト いいね 通知
はじめに
1.アプリケーション紹介
2.認証機能
3.TMDB api fetch
4.auto-suggestion検索フォーム
5.ジャンルフィルターとページ移動
6.リスト いいね 通知今回はログイン後の機能でリストといいねと公開前の映画を通知希望のリストに追加するとヘッダーで通知するようにします。
コードに関しては、githubにて後悔しておりますので、こちらからどうぞ!リスト
こんな感じでユーザーリストを作理、リストに追加できるようにします。
まずはじめにfirestoreないから、、、
firebase内のデータベース設計
ユーザー1人に、folderコレクションを作ります。
そこにはユーザーとは別のコレクションのfolderに映画をセットしていくようになります。下記がユーザーの中のfolderコレクションなります。こちらのidからfolderコレクションと紐付けしていきます。
そしてこちらがユーザーとは別のfolderコレクションの中身で実際にはこちらに映画をセットしていきます。
リスト作成
export const makeFolder = (uid: string, folderName: string) => { return async (dispatch:any) => { const ref = folderRef.doc() const folderId = ref.id folderRef.doc(folderId).set({ created_at: FirebaseTimestamp.now(), name: folderName, uid: uid, }) usersRef.doc(uid).collection('folder').doc(folderId).set({ name: folderName, id: folderId, created_at: FirebaseTimestamp.now(), }) } }まずfolderコレクションに作成日時とfolderの名前と作成者をセット、
そしてユーザーのfolderコレクションにも紐付けを行うためにfolderIdをセットします。リスト削除
export const deleteFolder = (uid: string, folderId: string) => { return async (dispatch:any) => { folderRef.doc(folderId).delete() usersRef.doc(uid).collection('folder').doc(folderId).delete() } }削除ボタンを押すと削除できるようにこちらも書いておきます。
リストへの追加
export const addFolderMovie = (folderId: string, movie:movie) => { return async (dispatch: any) => { folderRef.doc(folderId).collection('movie').get() .then(snapshot => { const match = [] snapshot.docs.forEach(doc => { const data = doc.data() if(data.id === movie.id){ match.push(data) } }) if(match.length > 0){ return false }else{ const ref = usersRef.doc(folderId).collection('movie').doc() const movieId = ref.id const data = { movieId: movieId, id: movie.id, title: movie.title, poster_path: movie.poster_path, backdrop_path: movie.backdrop_path, release_date: movie.release_date, genres: movie.genres, overview: movie.overview, vote_average: movie.vote_average, timestamp: FirebaseTimestamp.now() } folderRef.doc(folderId).collection('movie').doc(movieId).set(data) } }) } }これがfolderへの追加のコードですが、同じ映画がセットされることのないように一度、追加するfolder内の映画を取得して入ってきた映画とかぶっていないか確認しています。
そして被っていなければ映画をセットするようにしています。リストから映画を削除
export const deleteFolderMovie = (folderId:string, movie: movie) => { return async (dispatch:any) => { folderRef.doc(folderId).collection('movie').get() .then(snapshot => { const match:any = [] snapshot.docs.forEach(doc => { const data = doc.data() if(data.id === movie.id){ match.push(data) } }) if(match.length === 0){ return false }else{ folderRef.doc(folderId).collection('movie').doc(match[0].movieId).delete() } }) } }こちらでも同様に削除するものがなければfalseを返しています。
そしてあればfirestore内から削除するようにしています。これがリストの作成、削除、映画の追加、削除の処理になります。
いいね
こちらはユーザーログイン時にデフォルトで入っているお気に入り機能になります。
そのため、先ほどのリストへ映画追加と削除のコードとほとんど同じですので詳しい説明は省きます。export const deleteFavoriteMovie = (id: number) => { return async (dispatch:any, getState:any) => { const uid = getState().user.uid usersRef.doc(uid).collection('favorite').get() .then(snapshot => { const match:any = [] snapshot.docs.filter(doc => { const data = doc.data() if(data.id === id){ match.push(data) } }) if(match.length === 0){ return false }else{ console.log(match) usersRef.doc(uid).collection('favorite').doc(match[0].movieId).delete() } }) } } export const addFavoriteMovie = (movie: movie) => { return async (dispatch: any, getState:any) => { const uid = getState().user.uid usersRef.doc(uid).collection('favorite').get() .then(snapshot => { const match = snapshot.docs.filter(doc => { const data = doc.data() return data.id === movie.id }) if(match.length > 0){ return false }else{ const ref = usersRef.doc(uid).collection('favorite').doc() const movieId = ref.id const data = { movieId: movieId, id: movie.id, title: movie.title, poster_path: movie.poster_path, backdrop_path: movie.backdrop_path, release_date: movie.release_date, genres: movie.genres, overview: movie.overview, vote_average: movie.vote_average, timestamp: FirebaseTimestamp.now() } usersRef.doc(uid).collection('favorite').doc(movieId).set(data) } }) } }公開作品通知
ユーザーコレクションのnotificationコレクションを用意、こちらも同様に初ログインからデフォルトで入っているリストになります。
そして未公開の場合にだけこちらのリストに追加できるようにします。
これに関しては、映画のfetchした情報の中に公開日も入っているので、その日にちがまだきていなければnotificationコレクションへの追加ボタンを用意します。今回のコードはいいねの追加と全く同じなので追加と削除のコードは省きます。
firestoreからnotificatioコレクションをfetchしてきて公開日から一週間を切った場合、ヘッダーから通知されるようになります。
const [message, setMessage] = useState("") useEffect(() => { const release = movie.release_date.split('-') const year = release[0] const month = release[1] const date = release[2] const releaseDate = `${year}/${month}/${date} 00:00:00` let today:any = new Date() const data:any = Date.parse(releaseDate) const item = data - today if(item > 0){ if(item < 86400000){ setMessage("明日公開!!") }else if(item < 172800000){ setMessage('残り2日!') }else if(item < 259200000){ setMessage('残り3日!') }else if(item < 345600000){ setMessage('残り4日!') }else if(item < 432000000){ setMessage('残り5日!') }else if(item < 518400000){ setMessage('残り6日!') }else if(item < 604800000){ setMessage('残り7日!') } }else{ if(item > -604800000){ setMessage('公開中') }else{ db.collection('user').doc(displayUid).collection('notification').doc(movie.movieId).delete() } } },[])ここでは、fetchした映画の配列を回し、映画オブジェクトを渡されている状態です。
そして渡ってきた映画が公開日から何日過ぎているか残り7日から通知するようになっています。
公開まで7日以上の場合は、通知されないようになっています。
公開してから一週間がすぎると、自動でnotificationコレクションから削除するようします。終わりに
今回でこのアプリについての記事を終わろうと思います。
主な機能の実装方法のみ記事にしております。ので他の詳しいコードなどが知りたい方はこちらのgithubからどうぞ!今回、UI構築は、material-uiをふんだんに使いました。
そのおかげでデザインの知識がない私でも、充実したものになったので、本当に便利だと感じました。
そしてそのデザインもTMDB公式のアプリを大いに似せていただきました。これから勉強を重ねていきたいと思っております。
- 投稿日:2020-10-23T21:19:27+09:00
6.TMDB リスト いいね 通知
はじめに
1.アプリケーション紹介
2.認証機能
3.TMDB api fetch
4.auto-suggestion検索フォーム
5.ジャンルフィルターとページ移動
6.リスト いいね 通知今回はログイン後の機能でリストといいねと公開前の映画を通知希望のリストに追加するとヘッダーで通知するようにします。
コードに関しては、githubにて後悔しておりますので、こちらからどうぞ!リスト
こんな感じでユーザーリストを作理、リストに追加できるようにします。
まずはじめにfirestoreないから、、、
firebase内のデータベース設計
ユーザー1人に、folderコレクションを作ります。
そこにはユーザーとは別のコレクションのfolderに映画をセットしていくようになります。下記がユーザーの中のfolderコレクションなります。こちらのidからfolderコレクションと紐付けしていきます。
そしてこちらがユーザーとは別のfolderコレクションの中身で実際にはこちらに映画をセットしていきます。
リスト作成
export const makeFolder = (uid: string, folderName: string) => { return async (dispatch:any) => { const ref = folderRef.doc() const folderId = ref.id folderRef.doc(folderId).set({ created_at: FirebaseTimestamp.now(), name: folderName, uid: uid, }) usersRef.doc(uid).collection('folder').doc(folderId).set({ name: folderName, id: folderId, created_at: FirebaseTimestamp.now(), }) } }まずfolderコレクションに作成日時とfolderの名前と作成者をセット、
そしてユーザーのfolderコレクションにも紐付けを行うためにfolderIdをセットします。リスト削除
export const deleteFolder = (uid: string, folderId: string) => { return async (dispatch:any) => { folderRef.doc(folderId).delete() usersRef.doc(uid).collection('folder').doc(folderId).delete() } }削除ボタンを押すと削除できるようにこちらも書いておきます。
リストへの追加
export const addFolderMovie = (folderId: string, movie:movie) => { return async (dispatch: any) => { folderRef.doc(folderId).collection('movie').get() .then(snapshot => { const match = [] snapshot.docs.forEach(doc => { const data = doc.data() if(data.id === movie.id){ match.push(data) } }) if(match.length > 0){ return false }else{ const ref = usersRef.doc(folderId).collection('movie').doc() const movieId = ref.id const data = { movieId: movieId, id: movie.id, title: movie.title, poster_path: movie.poster_path, backdrop_path: movie.backdrop_path, release_date: movie.release_date, genres: movie.genres, overview: movie.overview, vote_average: movie.vote_average, timestamp: FirebaseTimestamp.now() } folderRef.doc(folderId).collection('movie').doc(movieId).set(data) } }) } }これがfolderへの追加のコードですが、同じ映画がセットされることのないように一度、追加するfolder内の映画を取得して入ってきた映画とかぶっていないか確認しています。
そして被っていなければ映画をセットするようにしています。リストから映画を削除
export const deleteFolderMovie = (folderId:string, movie: movie) => { return async (dispatch:any) => { folderRef.doc(folderId).collection('movie').get() .then(snapshot => { const match:any = [] snapshot.docs.forEach(doc => { const data = doc.data() if(data.id === movie.id){ match.push(data) } }) if(match.length === 0){ return false }else{ folderRef.doc(folderId).collection('movie').doc(match[0].movieId).delete() } }) } }こちらでも同様に削除するものがなければfalseを返しています。
そしてあればfirestore内から削除するようにしています。これがリストの作成、削除、映画の追加、削除の処理になります。
いいね
こちらはユーザーログイン時にデフォルトで入っているお気に入り機能になります。
そのため、先ほどのリストへ映画追加と削除のコードとほとんど同じですので詳しい説明は省きます。export const deleteFavoriteMovie = (id: number) => { return async (dispatch:any, getState:any) => { const uid = getState().user.uid usersRef.doc(uid).collection('favorite').get() .then(snapshot => { const match:any = [] snapshot.docs.filter(doc => { const data = doc.data() if(data.id === id){ match.push(data) } }) if(match.length === 0){ return false }else{ console.log(match) usersRef.doc(uid).collection('favorite').doc(match[0].movieId).delete() } }) } } export const addFavoriteMovie = (movie: movie) => { return async (dispatch: any, getState:any) => { const uid = getState().user.uid usersRef.doc(uid).collection('favorite').get() .then(snapshot => { const match = snapshot.docs.filter(doc => { const data = doc.data() return data.id === movie.id }) if(match.length > 0){ return false }else{ const ref = usersRef.doc(uid).collection('favorite').doc() const movieId = ref.id const data = { movieId: movieId, id: movie.id, title: movie.title, poster_path: movie.poster_path, backdrop_path: movie.backdrop_path, release_date: movie.release_date, genres: movie.genres, overview: movie.overview, vote_average: movie.vote_average, timestamp: FirebaseTimestamp.now() } usersRef.doc(uid).collection('favorite').doc(movieId).set(data) } }) } }公開作品通知
ユーザーコレクションのnotificationコレクションを用意、こちらも同様に初ログインからデフォルトで入っているリストになります。
そして未公開の場合にだけこちらのリストに追加できるようにします。
これに関しては、映画のfetchした情報の中に公開日も入っているので、その日にちがまだきていなければnotificationコレクションへの追加ボタンを用意します。今回のコードはいいねの追加と全く同じなので追加と削除のコードは省きます。
firestoreからnotificatioコレクションをfetchしてきて公開日から一週間を切った場合、ヘッダーから通知されるようになります。
const [message, setMessage] = useState("") useEffect(() => { const release = movie.release_date.split('-') const year = release[0] const month = release[1] const date = release[2] const releaseDate = `${year}/${month}/${date} 00:00:00` let today:any = new Date() const data:any = Date.parse(releaseDate) const item = data - today if(item > 0){ if(item < 86400000){ setMessage("明日公開!!") }else if(item < 172800000){ setMessage('残り2日!') }else if(item < 259200000){ setMessage('残り3日!') }else if(item < 345600000){ setMessage('残り4日!') }else if(item < 432000000){ setMessage('残り5日!') }else if(item < 518400000){ setMessage('残り6日!') }else if(item < 604800000){ setMessage('残り7日!') } }else{ if(item > -604800000){ setMessage('公開中') }else{ db.collection('user').doc(displayUid).collection('notification').doc(movie.movieId).delete() } } },[])ここでは、fetchした映画の配列を回し、映画オブジェクトを渡されている状態です。
そして渡ってきた映画が公開日から何日過ぎているか残り7日から通知するようになっています。
公開まで7日以上の場合は、通知されないようになっています。
公開してから一週間がすぎると、自動でnotificationコレクションから削除するようします。終わりに
今回でこのアプリについての記事を終わろうと思います。
主な機能の実装方法のみ記事にしております。ので他の詳しいコードなどが知りたい方はこちらのgithubからどうぞ!今回、UI構築は、material-uiをふんだんに使いました。
そのおかげでデザインの知識がない私でも、充実したものになったので、本当に便利だと感じました。
そしてそのデザインもTMDB公式のアプリを大いに似せていただきました。これから勉強を重ねていきたいと思っております。
- 投稿日:2020-10-23T21:19:02+09:00
4.TMDB ジャンルフィルターとページ移動
はじめに
1.アプリケーション紹介
2.認証機能
3.TMDB api fetch
4.ジャンルフィルターとページ移動
5.リスト いいね 通知アプリの概要など他にもこのアプリケーションについての記事を載せておりますのでそちらの方がみたい方はそちらをどうぞ!
コードもgithubにてのせておりますので、こちらからどうぞ!
今回は、ジャンルを選択したときにフィルターにかけてapiをたたいて映画の取得を行います。そしてページ遷移ができるコードも載せていきたい思っております。
ジャンルフィルター
こんな感じのジャンルフィルターを作っていきます。
//selectGenreは、ジャンルボタンを選択した時にそのジャンルをこのstateにせっとする。この中にジャンルが入ってくる! const [selectGenre, setSelectGenre] = useState<genre[]>([]) //ジャンルボタンの選択後の挙動 const toggleGenre = (genre: genre) => { const filteredGenres = selectGenre.filter((g:genre) => g.id !== genre.id) if(filteredGenres.length === selectGenre.length){ setSelectGenre([ ...filteredGenres, genre, ]) }else{ setSelectGenre([ ...filteredGenres, ]) } } //selectGenreがセットされるたびにfetchするようにする useEffect(() => { const genreIDs = selectGenre.map((g: genre):number => { return g.id }) if(path === '/'){ dispatch(fetchMovieList(API_GET_MOVIE_POPULAR, genreIDs)) }else if(path === '/upcoming'){ dispatch(fetchMovieList(API_GET_MOVIE_UPCOMING, genreIDs)) }else if(path === '/now_playing'){ dispatch(fetchMovieList(API_GET_MOVIE_NOW_PLAYING, genreIDs)) }else if(path === '/top_rated'){ dispatch(fetchMovieList(API_GET_MOVIE_TOP_RATED, genreIDs)) } },[selectGenre, path]) export const API_GET_MOVIE_POPULAR = 'movie/popular'; export const API_GET_MOVIE_UPCOMING = 'movie/upcoming' export const API_GET_MOVIE_NOW_PLAYING = 'movie/now_playing' export const API_GET_MOVIE_TOP_RATED = 'movie/top_rated'1.selectGenreをフィルターにかけて、一個目にセットしたジャンルとかぶっていた場合は、それ以外をstateにセットする、またかぶっていなければそのままstateをセット
2.そしてselectGenreのstateがセットされるごとにデータをfetchする(ジャンルボタンをクリック)
今回は、公開中(/now_playing)、人気(/)、高評価(top_rated)、新作公開(upcoming)も同じコンポーネントで表示するのでパスによってfetchするときのURIが変わるようにしています。ページ遷移
//次にページをセット const prevPage = (page - 1) <= 0 ? 1 : (page - 1); //ページを一個戻る挙動 const nextPage = (page + 1) > total_pages ? total_pages : (parseInt(Page, 10) + parseInt('1', 10)); //そして次へボタンへのchangePageは、nextPageを戻るボタンへのchangePageは、prevPageを渡す const changePage = (page: number) => { if(page === 0){ alert('該当の作品はありませんでした。') return false }else{ if(typeof(Storage) !== 'undefined'){ localStorage.setItem('currentPage', JSON.stringify(page)) } const GenresID = selectGenre.map((g:genre) => g.id) if(path === '/'){ dispatch(fetchMovieList(API_GET_MOVIE_POPULAR, GenresID, page)) }else if(path === '/upcoming'){ dispatch(fetchMovieList(API_GET_MOVIE_UPCOMING, GenresID, page)) }else if(path === '/now_playing'){ dispatch(fetchMovieList(API_GET_MOVIE_NOW_PLAYING, GenresID, page)) }else if(path === '/top_rated'){ dispatch(fetchMovieList(API_GET_MOVIE_TOP_RATED, GenresID, page)) } } } return( <button type="button" title="Previous 20 movies" onClick={() => changePage(prevPage)} > Prev </button> <div> {page} <span> / </span> {total_pages} </div> <button type="button" title="Next 20 movies" onClick={() => changePage(nextPage)} > Next </button> )1.prevPageは、page数が1以外の場合は、pageステートを-1する
2.nextPageは、fetchしてきた情報の中にトータルのページ数も入っているので、そのトータルページ数とpageステートが同じではない場合は、pageを+1するようになっている。
3.このprevPageは、戻るボタンをクリックした時にchangePageの引数として渡す。次へボタンを押したらnextPageを下記のように引数として渡す。
4.前、次のページのボタンがクリックされるとpageステートがそれに応じて変化してそれを各pathの映画のfetchメソッドに渡すことでpageを戻ったり、次へ進んだりできるようになる。
- 投稿日:2020-10-23T21:19:02+09:00
5.TMDB ジャンルフィルターとページ移動
はじめに
1.アプリケーション紹介
2.認証機能
3.TMDB api fetch
4.auto-suggestion検索フォーム
5.ジャンルフィルターとページ移動
6.リスト いいね 通知アプリの概要など他にもこのアプリケーションについての記事を載せておりますのでそちらの方がみたい方はそちらをどうぞ!
コードもgithubにてのせておりますので、こちらからどうぞ!
今回は、ジャンルを選択したときにフィルターにかけてapiをたたいて映画の取得を行います。そしてページ遷移ができるコードも載せていきたい思っております。
ジャンルフィルター
こんな感じのジャンルフィルターを作っていきます。
//selectGenreは、ジャンルボタンを選択した時にそのジャンルをこのstateにせっとする。この中にジャンルが入ってくる! const [selectGenre, setSelectGenre] = useState<genre[]>([]) //ジャンルボタンの選択後の挙動 const toggleGenre = (genre: genre) => { const filteredGenres = selectGenre.filter((g:genre) => g.id !== genre.id) if(filteredGenres.length === selectGenre.length){ setSelectGenre([ ...filteredGenres, genre, ]) }else{ setSelectGenre([ ...filteredGenres, ]) } } //selectGenreがセットされるたびにfetchするようにする useEffect(() => { const genreIDs = selectGenre.map((g: genre):number => { return g.id }) if(path === '/'){ dispatch(fetchMovieList(API_GET_MOVIE_POPULAR, genreIDs)) }else if(path === '/upcoming'){ dispatch(fetchMovieList(API_GET_MOVIE_UPCOMING, genreIDs)) }else if(path === '/now_playing'){ dispatch(fetchMovieList(API_GET_MOVIE_NOW_PLAYING, genreIDs)) }else if(path === '/top_rated'){ dispatch(fetchMovieList(API_GET_MOVIE_TOP_RATED, genreIDs)) } },[selectGenre, path]) export const API_GET_MOVIE_POPULAR = 'movie/popular'; export const API_GET_MOVIE_UPCOMING = 'movie/upcoming' export const API_GET_MOVIE_NOW_PLAYING = 'movie/now_playing' export const API_GET_MOVIE_TOP_RATED = 'movie/top_rated'1.selectGenreをフィルターにかけて、一個目にセットしたジャンルとかぶっていた場合は、それ以外をstateにセットする、またかぶっていなければそのままstateをセット
2.そしてselectGenreのstateがセットされるごとにデータをfetchする(ジャンルボタンをクリック)
今回は、公開中(/now_playing)、人気(/)、高評価(top_rated)、新作公開(upcoming)も同じコンポーネントで表示するのでパスによってfetchするときのURIが変わるようにしています。ページ遷移
//次にページをセット const prevPage = (page - 1) <= 0 ? 1 : (page - 1); //ページを一個戻る挙動 const nextPage = (page + 1) > total_pages ? total_pages : (parseInt(Page, 10) + parseInt('1', 10)); //そして次へボタンへのchangePageは、nextPageを戻るボタンへのchangePageは、prevPageを渡す const changePage = (page: number) => { if(page === 0){ alert('該当の作品はありませんでした。') return false }else{ if(typeof(Storage) !== 'undefined'){ localStorage.setItem('currentPage', JSON.stringify(page)) } const GenresID = selectGenre.map((g:genre) => g.id) if(path === '/'){ dispatch(fetchMovieList(API_GET_MOVIE_POPULAR, GenresID, page)) }else if(path === '/upcoming'){ dispatch(fetchMovieList(API_GET_MOVIE_UPCOMING, GenresID, page)) }else if(path === '/now_playing'){ dispatch(fetchMovieList(API_GET_MOVIE_NOW_PLAYING, GenresID, page)) }else if(path === '/top_rated'){ dispatch(fetchMovieList(API_GET_MOVIE_TOP_RATED, GenresID, page)) } } } return( <button type="button" title="Previous 20 movies" onClick={() => changePage(prevPage)} > Prev </button> <div> {page} <span> / </span> {total_pages} </div> <button type="button" title="Next 20 movies" onClick={() => changePage(nextPage)} > Next </button> )1.prevPageは、page数が1以外の場合は、pageステートを-1する
2.nextPageは、fetchしてきた情報の中にトータルのページ数も入っているので、そのトータルページ数とpageステートが同じではない場合は、pageを+1するようになっている。
3.このprevPageは、戻るボタンをクリックした時にchangePageの引数として渡す。次へボタンを押したらnextPageを下記のように引数として渡す。
4.前、次のページのボタンがクリックされるとpageステートがそれに応じて変化してそれを各pathの映画のfetchメソッドに渡すことでpageを戻ったり、次へ進んだりできるようになる。
- 投稿日:2020-10-23T21:18:12+09:00
【初心者でもわかる】select要素を使わず、divでselectみたいな動きを作る
どうも7noteです。select要素にcssが使いにくいので、他の方法を考えてみました。
select要素にはCSSが調整難しいため、自由に調整できるdiv要素でできる方法を書いていきます。
また、PCとスマホでselect要素風とそうじゃない動きに切り分けるなどが必要な時にも使えると思います。書き方
※jQueryを使用しています。
index.html<ul> <li class="check" link="no1" style="display: list-item;">その1</li> // linkの値と、pタグのクラスを揃える <li link="no2">その2</li> <li link="no3">その3</li> <li link="no4">その4</li> <li link="no5">その5</li> </ul> <p class="no1">テキスト1</p> <p class="no2">テキスト2</p> <p class="no3">テキスト3</p> <p class="no4">テキスト4</p> <p class="no5">テキスト5</p>style.cssul { width: 200px; /* セレクトボックスの横幅 */ border: solid 1px #333; /* 見やすく境界線を引く */ position: absolute; /* 選択肢が開いた時に高さが変わるので指定 */ top: 0px; /* 好きな位置に */ left: 0px; /* 好きな位置に */ } ul li { padding: 5px 10px 5px 20px; /* optionの余白と同等 */ display: none; /* 最初は非表示 */ list-style: none; /* 「・」を非表示にする */ } ul li.check { color: #fff; /* 選択されたもののみ装飾 */ font-weight: bold; /* 選択されたもののみ装飾 */ background: #999; /* 選択されたもののみ装飾 */ } p { display: none; /* 最初は非表示 */ margin-left: 220px; /* セレクトボックスとかぶらないように位置調整 */ }script.js$(function () { var click_flg = true; // クリックを許可する変数を設定 $('.check').show(); // ページ読み込み時、任意のselect1つだけ表示 $('.no1').show(); // ページ読み込み時、任意のテキスト1つだけ表示 $('ul li').on('click', function(){ // セレクトボックスのどれかがクリックされた時 if(click_flg){ // クリックが許可されているかどうか click_flg = false; // ボタンを一時的に無効 $('ul li').removeClass('check'); // 全てのliからcheckを削除してから、 $(this).addClass('check'); // 選択されたものにcheckのクラスを付ける $('ul li').not('.check').fadeToggle(400, function() { // check以外の表示と非表示を切り替える click_flg = true; // コールバック関数を使い、アニメーションが終わってからtrueにするように指定 }); $('p').hide(); // pを全て非表示 $('.' + $(this).attr('link')).show(); // selectされているlinkと同じクラスをもつpだけ表示 } }); });解説
各行で行なっている動きはコメントでご確認ください。
大まかな処理の流れとしては、、、① ページ読み込み時、任意のselectとテキストを表示。
② li要素がクリックされた時、他のliを表示状態に切り換え(fadeToggle)
③ liが全て開いている状態の時、liがクリックされたら、クリックされたものにのみcheckのクラスを付与、かつ他のliを非表示状態に切り換え。
④ また同時に、pを全て非表示にしてクリックされたliのlinkと同じクラスを持つpだけ表示。
⑤ 結果、選択したli要素と、紐づいているp要素のみ表示状態になる。そして、click_flg変数を設定しておくことで、フェードインアウトの処理中にクリックされても不具合を起こさないように処理をしています。
まとめ
正直な話をするとselectboxを使うほうが早いですし、わりと無理な作りになっていると思うので、
「どうしてもdivでselectのような動きを実装したいんだぁぁぁ」
ってときにだけお使いください。
おそまつ!
~ Qiitaで毎日投稿中!! ~
【初心者向け】HTML・CSSのちょいテク詰め合わせ
- 投稿日:2020-10-23T21:15:54+09:00
3.TMDB データfetch
はじめに
1.アプリケーション紹介
2.認証機能
3.TMDB api fetch
4.auto-suggestion検索フォーム
5.ジャンルフィルターとページ移動
6.リスト いいね 通知今回は実際にapi叩く処理を載せていきます。
tmdbのapiは充実しているのでより詳しく映画の詳細の表示やフィルタリングができました。
どんなアプケーションかは、1回目の記事にて載せておりますのでみていただけると幸いです。コードがあればいいという方は、こちらからどうぞ!
概要
今回は、新作公開(upcoming)、人気(popular)、公開中(now_playing)、高評価(top_rated)を映画のリストを表示できるようにします。
それごとにジャンルのフィルターをかけられるようにしています。映画リストfetch
export const fetchMovieList = (API_GET_MOVIE_BY = API_GET_MOVIE_POPULAR, genreIDs: number[], page = 1) => { const genreParams = genreIDs ? `${API_PARAMS_GENRE}${genreIDs.join('%2C')}` : ''; return async (dispatch: any) => { dispatch(fetchMovie()) return fetch(`${URL}${API_GET_MOVIE_BY}${API_KEY}${API_PARAMS_PAGE}${page}${genreParams}`) .then(response => response.json()) .then(json => dispatch(fetchMovieSuccess(json.results, json.page, json.total_pages))) .catch(error => dispatch(fetchMovieFailure(error))) } } export const URL = 'https://api.themoviedb.org/3/'; export const API_GET_MOVIE_POPULAR = 'movie/popular'; export const API_GET_MOVIE_UPCOMING = 'movie/upcoming' export const API_GET_MOVIE_NOW_PLAYING = 'movie/now_playing' export const API_GET_MOVIE_TOP_RATED = 'movie/top_rated'こちらが、映画のジャンルのリストを叩く処理になります。
最初に引数であるAPI_GET_MOVIE_BYは、デフォルトだと人気popularのapiを叩くので、デフォルトはpopularのapiを設定しておきます。genresIDsは、ジャンル選択した時にgenreIDsを引数でわたします。page遷移も行いますので最初はデフォルトの1を設定しておきます。
https://api.themoviedb.org/3/movie/now_playing?api_key=API_KEY&page=1&with_genres=99%2C35
urlは上記の形で叩くのでgenresのパラメータでは、genreIdごとに%2Cを入れなければいけないので、genreParamsにて代入します。
fetchしている時、fetchが成功した時、失敗した時にそれぞれreducerに渡してstoreで更新します。映画検索fetch
export const searchMovieList = (keyword: string) => { let url = URL_SEARCH + keyword + API_KEY_ALT; return async (dispatch: any) => { dispatch(searchMovie(keyword)) return fetch(url) .then(response => response.json()) .then(json => json.results) .then(data => dispatch(searchMovieSuccess(data, keyword))) .catch(error => dispatch(searchMovieFailure(error))) } } export const URL_SEARCH = 'https://api.themoviedb.org/3/search/movie?query=';映画詳細関連fetch
export const fetchMovieDetail = (id: string) => { const url_movie = URL_DETAIL + id + API_KEY; return async (dispatch: any) => { dispatch(fetchMovieDetailAction()) return fetch(url_movie) .then(response => response.json()) .then(data => dispatch(fetchMovieDetailSuccess(data))) .catch(error => dispatch(fetchMovieDetailFailure(error))) } } export const URL_DETAIL = 'https://api.themoviedb.org/3/movie/';まずここでは、映画のidを使って映画の詳細取得します。
そしてその取得後の情報をもとに、youtubeの関連動画、俳優リスト、関連映画の取得を行います。export const fetchTrailerList = (id: string) => { const url_trailers = URL_DETAIL + id + URL_VIDEO + API_KEY; return async (dispatch: any) => { dispatch(fetchTrailers()) return fetch(url_trailers) .then(response => response.json()) .then(json => json.results) .then(data => { let youtubeTrailers = data.filter((trailer:any) => { return trailer.site === 'YouTube'; }) dispatch(fetchTrailersSuccess(youtubeTrailers)); }) .catch(error => dispatch(fetchTrailersFailure(error))) } } export const fetchCastList = (id: string) => { const url_casts = URL_DETAIL + id + URL_CAST + API_KEY; return async (dispatch: any) => { dispatch(fetchCasts()) return fetch(url_casts) .then(response => response.json()) .then(json => json.cast) .then(data => dispatch(fetchCastsSuccess(data))) .catch(error => dispatch(fetchCastsFailure(error))) } } export const fetchSimilarMovies = (movieID: string) => { let url = URL + API_GET_MOVIE_SIMILAR(movieID) + API_KEY return async (dispatch: any) => { dispatch(fetchMovie()) return fetch(url) .then(responnse => responnse.json()) .then(json => json.results) .then(data => dispatch(fetchMovieSuccess(data, 0, 0))) .catch(error => dispatch(fetchMovieFailure(error))) } } export const URL_VIDEO = '/videos'; export const URL_CAST = '/casts'; export const URL_DETAIL = 'https://api.themoviedb.org/3/movie/'; export const URL = 'https://api.themoviedb.org/3/'; export const API_GET_MOVIE_SIMILAR = (movieID: any) => `movie/${movieID}/similar`;tmdbのapiでは、関連の予告動画などやキャストや似ている映画などのapiも続けて叩けるようになっているので、それをつかって
より詳しく映画の詳細を表示できるようになっています。
映画の詳細はこんな感じです!俳優詳細fetch
export const fetchActorDetail = (id: string) => { const url_actor = URL_PERSON + id + API_KEY; return async (dispatch:any) => { dispatch(fetchActor()) return fetch(url_actor) .then(response => response.json()) .then(data => dispatch(fetchActorSuccess(data))) .catch(error => dispatch(fetchActorFailure(error))) } } export const URL_PERSON = 'https://api.themoviedb.org/3/person/';actorIdを使って俳優の詳細を取得します。
そしてその俳優の出演作品の情報を取得しますexport const fetchActorMovieList = (id:string) => { let url: string; if(id)url = URL_LIST + API_KEY + '&with_cast=' + id; else url = URL_LIST + API_KEY; return async (dispatch:any) => { dispatch(fetchMovie()); return fetch(url) .then(response => response.json()) .then(json => json.results) .then(data => dispatch(fetchMovieSuccess(data, 0, 0))) .catch(error => dispatch(fetchMovieFailure(error))) } } export const URL_LIST = 'https://api.themoviedb.org/3/discover/movie';こんな感じで表示します。デザインはTMDBに似せております。
ページのデザインやなどはgithubのコードにて確認できます。
ここでは省かせていただきます。
- 投稿日:2020-10-23T21:12:57+09:00
1.TMDB apiを使ったアプリケーション紹介
はじめに
1.アプリケーション紹介
2.認証機能
3.TMDB api fetch
4.auto-suggestion検索フォーム
5.ジャンルフィルターとページ移動
6.リスト いいね 通知今回TMDBのapiを使って映画検索のアプケーションを作ってみました。
cssの勉強不足なのでデザインに関しては、TMDBのアプリケーションをまねて作らせていただきました。
こちらになります。
プログラミング勉強したての初心者の制作物になりますので、暖かくみていただけると幸いです。
githubにて公開もしておりますので、コードのみ知りたい方は以下からどうぞ!!
https://github.com/yuuki008/movie-box今回は、単にどういったアプケーションかの紹介になります。
主な機能紹介
ジャンルボタンをクリックすることで、TMDBapiを叩いて映画を取得できるようにします。
人気だけでなく、公開中、高評価、新作公開のapiもあったので、それごとにapiに上記のようなジャンルフィルターに掛けられるようにしています。
つぎに認証機能にログインするといいねとリスト作成、追加、削除ができます。
映画の検索フォームに入力すると1文字ごとにapiを叩き、検索結果が出力されるようになっています。
以上が大体のアプリ紹介になります。
使ったパッケージ
"@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/styles": "^4.10.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "connected-react-router": "^6.8.0", "firebase": "^7.21.0", "history": "^4.10.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-notification-system": "^0.4.0", "react-redux": "^7.2.1", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-scripts": "3.4.3", "redux": "^4.0.5", "redux-action": "^1.2.2", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", "reselect": "^4.0.0", "router": "^1.3.5", "thunk": "0.0.1"ディレクトリ構成
├── AuthWrapper.tsx ├── api.tsx ├── assets │ ├── actor.css │ ├── genreList.css │ ├── images │ │ ├── logo.svg │ │ ├── logo_square.svg │ │ ├── no_image.png │ │ ├── themoviedb.png │ │ └── themoviedb_green.svg │ ├── movieDetail.css │ ├── pageButton.css │ ├── profile.css │ └── search.css ├── components │ ├── Card │ │ ├── Cast.tsx │ │ ├── DefaultCard.tsx │ │ ├── MovieCard.tsx │ │ ├── MovieCard2.tsx │ │ └── Trailer.tsx │ ├── Modal │ │ └── FolderList.tsx │ ├── PageComponent │ │ ├── Favorite.tsx │ │ ├── FolderMovie.tsx │ │ ├── Genre.tsx │ │ ├── Header.tsx │ │ └── Release.tsx │ ├── UIkit │ │ ├── BoxLabel.tsx │ │ ├── FormControl.tsx │ │ ├── LightTooltip.tsx │ │ ├── MenuButton.tsx │ │ ├── Notification.tsx │ │ ├── PageButton.tsx │ │ ├── PrimaryButton.tsx │ │ ├── RatingStar.tsx │ │ ├── ReleaseMovie.tsx │ │ ├── SelectBox.tsx │ │ ├── Suggestion.tsx │ │ ├── TextInput.tsx │ │ └── index.tsx │ └── index.ts ├── containers │ ├── Actor.tsx │ ├── Auth │ │ ├── Reset.tsx │ │ ├── SignIn.tsx │ │ ├── SignUp.tsx │ │ └── index.tsx │ ├── MovieContainer.tsx │ ├── MovieDetail.tsx │ ├── MyList.tsx │ └── index.ts ├── firebase │ ├── config.tsx │ └── index.tsx ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── redux │ ├── actor │ │ ├── actions.tsx │ │ ├── operations.tsx │ │ └── reducers.tsx │ ├── castlist │ │ ├── actions.tsx │ │ ├── operations.tsx │ │ └── reducers.tsx │ ├── folder │ │ ├── actions.tsx │ │ ├── operations.tsx │ │ └── reducers.tsx │ ├── movie │ │ ├── actions.tsx │ │ ├── operations.tsx │ │ └── reducer.tsx │ ├── movielist │ │ ├── actions.tsx │ │ ├── operations.tsx │ │ └── reducers.tsx │ ├── selectors.tsx │ ├── store.tsx │ ├── trailerlist │ │ ├── actions.tsx │ │ ├── operations.tsx │ │ └── reducers.tsx │ └── user │ ├── actions.tsx │ ├── operations.tsx │ └── reducers.tsx ├── serviceWorker.ts └── setupTests.tsこちらがpackage.jsonの中身になります。
認証機能とdatabase管理は、firebaseを使いました。
UI構築は、material-uiをふんだんに使いました。
今回は、typescriptを使いましたが、まだ勉強し始めのため、any型を多く使ってしまっております!!
申し訳ないです!!終わりに
今回は、主な機能の実装方法しか載せておりませんので、コードを全て細かく知りたい方はこちらからどうぞ!
ではあと4つ記事を上げていきますので、みていただけると幸いです。
- 投稿日:2020-10-23T20:48:45+09:00
チームで出欠管理システムを作りました
社内チームで今年一年の成果物として出欠管理システムを作成しました!!
この記事ではその概要を紹介します。プロジェクト概要
成果物として何を作成するかチームで話し合い、社内全体で利用でき作業効率化につながるシステムについて考えました。
社内行事の出欠管理がExcel管理されており、作業負荷がかかる状態だったため、この改善としてQR認証を利用した出欠管理システムを作ることになりました。システム運用イメージ
開催者がイベント開催日を登録
開催者がイベントの出席状況を確認する
システムの使い方
QRコード発行
イベント登録/QRコード読み取り
イベント履歴確認
活用技術
メンバーの各々が得意な分野を用いて作成されています。
- 実行環境
- GCP
- Compute Engine
- Cloud DNS
- Cloud Load Balancing
- Docker
- Nginx
- PostgreSQL
- Webアプリ
- JavaScript
- node.js
- Vue.js
- TypeScript
- API
- Python
- Flask
システム構成
GCPを利用して環境を構築しました。
Web画面、API、DBをVM上にDockerコンテナとして常駐させています。
タブレットでブラウザからURLへアクセスするとDNS・LB・Nginxを経由、Web画面またはAPIへリクエストし、QRコード生成/認証・イベント履歴閲覧などの各処理を行う実装方式としています。
QR認証履歴などの各データへのアクセスはAPIからDBへSQLを発行することで保存するようにしました。開発期間
主担当3人で日常業務の片手間で約3か月ほどかかりました。
実際の作業時間はもっと短かったです。苦労した点
- 担当者3人で作った成果物の結合
インフラ面で統一した開発環境を用意せず進めたため、実際に結合した際にCORSの考慮漏れ等でwebアプリがうまく動作しませんでした。
躓いた点はIssueに整理して今後のチーム開発では気を付けていきます。- クライアントからAPIへのアクセス
Web画面からAPIにリクエストする際、クライアントからリクエストを飛ぶことを意識していなかったため、ファイアウォールやDNS・LB・Nginxの設定に苦労しました。
今後システム構成を考える際は意識していきたいと思います。今後の展開
- 管理画面へのログイン認証機能の実装
実装工数が足りなかったため実装できませんでした。
Amazon Cognitoを利用した実装を検討中です。- システム基盤をAWSへ移行
前述のAWS Cognito実装などAWSサービスの勉強も今後チームで実施していきたいと考えています。
併せて、基盤移行も検討中です。- 各社内イベントでの運用
コロナの影響で実際に人が集まる機会がなく、テストが不十分です。
データ量や負荷など分からない点が多いので、今後活用してもらっていきたいと考えています。
- 投稿日:2020-10-23T20:44:08+09:00
【JavaScript】Javascript使うなら知っておくべき型意識
あるレスポンス値から取得した配列から、ある条件に合致した要素オブジェクトを取得したい。
そんな時の条件式で、気をつけなければならないことをまとめます。
様々な条件式での取得
data () { return { staff_id: "", } }/// 取得できる const staff = res.data.find((staff) => staff.id == this.staff_id); /// 取得できる const staff = res.data.find((staff) => staff.id == Number(this.staff_id)); /// 取得できない const staff = res.data.find((staff) => staff.id === this.staff_id); /// 取得できる const staff = res.data.find((staff) => staff.id === Number(this.staff_id));型まで合致させるかさせないか
==は、型は一緒じゃなくてもいいが、中身が一緒であればtrue
===は、型も中身も一緒な場合のみ、trueつまり3番目の条件式では、中身は一緒だけど型まで一緒なやつはいないよという言われることになります。
型を合わせるメソッド
/// 文字列→数値 Number('123'); parseInt('123') /// 数値→文字列 String(123) 123.toString
- 投稿日:2020-10-23T20:08:25+09:00
predicateとは?表示されたときの対応方法
predicateとは?表示されたときの考え方
Google Chromeのconsoleで関数を入力した時に表示されるpredicateについて。
例えば、以下のように
findIndex関数を入力したときに表示される。vscodeだと以下。
predicateとは?
「真偽値を返す関数」のこと。
ググると述語などと出てしまう。プログラミング用語なので、wikipediaで見ると納得できる。
a predicate is commonly understood to be a Boolean-valued function P: X→ {true, false}
真偽値を返す関数とは?
よく使われるのは、比較を行っている式。(==, >, < など)
a = [1,3,2,5,4] a.findIndex((x)=> x==3) #1x==3かどうかを判定している。Yesならtrue、Noならfalseとなる。
アロー関数
((x)=> x==3)カッコ内の式はアロー関数。functionで書く関数を簡易化したもの。
xは任意の値。findIndexの場合、オブジェクトで指定した配列の要素が一つ一つ入る。(for文で回している状態と同じ)
このxが3と等しいかを判断してる。
アロー関数を使わない場合a.findIndex(function(x){ return (x==3) })
- 投稿日:2020-10-23T19:53:12+09:00
leafletでウェブページに地図を表示する
leafletとは
javascriptで地図表示をやってくれるライブラリです。
https://leafletjs.com/index.htmlタイルについて
ウェブで一般的な地図アプリケーションでは地図の画像をタイル状に敷き詰めて表示しています。leafletも同様に地図タイルを取得して表示するように作られていますので、無償または有償で提供されているタイルを利用して表示することができます。
Open Street Mapなど、無料で利用可能なタイル一覧を以下で確認することが出来ます
https://leaflet-extras.github.io/leaflet-providers/preview/また、mapboxなどの地図APIサービスのタイルを利用することも出来ます。mapboxを使用する場合はアカウントを作ってAPI-keyを生成して使う必要があります。
https://docs.mapbox.com/vector-tiles/reference/1. 地図を表示する
以下の4つを記載するだけです。
1. leafletのCSSを読み込む
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/>2. leafletのjavascriptコードを読み込む
これは上記のCSSの読み込みの後に書かないといけないようです
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>3. 地図を表示する場所にタグを書く
<div id="mapid"></div>4. 地図の表示幅を設定する
#mapid { height: 180px; }まとめて書くとこんな感じ
以下ではOSMのタイルを利用しています
index.html<!DOCTYPE html> <html lang="ja" dir="ltr"> <head> <meta charset="utf-8"> <title>Leafletデモ</title> </head> <body> <div id="mapid"></div> </body> </html> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css" integrity="sha512Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==" crossorigin="" /> <script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js" integrity="sha512-/Nsx9X4HebavoBvEBuyp3I7od5tA0UzAxs+j83KgC8PU0kgB4XiK4Lfe4y4cgBtaRJQEIFCW+oC506aPT2L1zw==" crossorigin=""></script> <style media="screen"> #mapid { height: 500px; } </style> <script type="text/javascript"> //地図の中心地を定義 var mymap = L.map('mapid').setView([37.85, 138.8], 9); //タイルレイヤーを取得 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: 'Map data © <a href="https://openstreetmap.org">OpenStreetMap</a>', maxZoom: 18}).addTo(mymap); </script>2. 要素を足す
マーカーを置く
var marker = L.marker([37.85, 138.8]).addTo(mymap);サークルを描く
var circle = L.circle([37.85, 139.1], { color: 'red', fillColor: '#f03', fillOpacity: 0.5, radius: 6000 }).addTo(mymap);ポリゴンを描く
geo_array = [[37.95, 138.6], [38.05, 138.3], [37.80, 138.4]] var polygon = L.polygon(geo_array, { color: 'green', fillColor: '#25dc25', fillOpacity: 0.5}).addTo(mymap);3. ポップアップを付ける
配置した部品に付ける
marker.bindPopup("<b>Hello world!</b><br>I am a popup.").openPopup(); circle.bindPopup("I am a circle."); polygon.bindPopup("I am a polygon.");マップ上の座標に付ける
var popup = L.popup() .setLatLng([37.4, 138.9]) .setContent("I am a standalone popup.") .openOn(mymap);イベントに対応して付ける
以下のようにクリックイベントが発生したらポップアップを呼ぶこともできます
var popup = L.popup(); function onMapClick(e) { popup .setLatLng(e.latlng) .setContent("You clicked the map at " + e.latlng.toString()) .openOn(mymap); } mymap.on('click', onMapClick);4. GeoJSONを使う
ポリゴン座標をJSONでまとめて渡すことも出来ます。
GeoJSONという書式があって、その通りに書いて渡すだけです。
https://leafletjs.com/examples/geojson/GeoJSON Specification (RFC 7946)
https://tools.ietf.org/html/rfc7946var myLines = [{ "type": "LineString", "coordinates": [[138.6, 37.95], [138.3, 38.05], [138.4, 37.80], [138.6, 37.95]] }, { "type": "LineString", "coordinates": [[139.1, 38.05], [138.8, 38.15], [138.9, 37.90], [139.1, 38.05]] }]; var myStyle = { "color": "#ff7800", "weight": 5, "opacity": 0.65 }; L.geoJSON(myLines, { style: myStyle }).addTo(mymap);色を指定する
L.geoJSONクラスを定義する際に色を指定すればその色で塗ってくれます。
var states = [{ "type": "Feature", "properties": {"party": "Republican"}, "geometry": { "type": "Polygon", "coordinates": [[ [-104.05, 48.99], [-97.22, 48.98], [-96.58, 45.94], [-104.03, 45.94], [-104.05, 48.99] ]] } }, { "type": "Feature", "properties": {"party": "Democrat"}, "geometry": { "type": "Polygon", "coordinates": [[ [-109.05, 41.00], [-102.06, 40.99], [-102.03, 36.99], [-109.04, 36.99], [-109.05, 41.00] ]] } }]; L.geoJSON(states, { style: function(feature) { switch (feature.properties.party) { case 'Republican': return {color: "#ff0000"}; case 'Democrat': return {color: "#0000ff"}; } } }).addTo(mymap);5. コロプレス図を作る
地図にヒートマップを表示するアレです
手順は以下のチュートリアルのものを参照しています
https://leafletjs.com/examples/choropleth/使用するgeoJSONファイル(us-states.js)は以下のような配置になっています。
us-states.js{ "type": "Feature", "properties": { "name": "Alabama", "density": 94.65 }, "geometry": ... ... }geoJSONを表示する
上記のus-states.jsをhtmlと同じフォルダに置いて読み込んで使います
index.html<script src="us-states.js"></script> <script type="text/javascript"> //地図の中心地を定義 var mymap = L.map('mapid').setView([37.8, -96], 4); //タイルレイヤーを取得 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: 'Map data © <a href="https://openstreetmap.org">OpenStreetMap</a>', maxZoom: 18}).addTo(mymap); //"https://leafletjs.com/examples/choropleth/us-states.js" L.geoJson(statesData).addTo(mymap); </script>色をつける
色や不透明度などを変更するにはL.geoJson()の第2引数にstyleを含むオプションを与えますが、ここに関数を書くことでgeoJSONの各feature内の値を参照して色を付けることが出来ます。以下ではfeature.properties.densityをfillColor()に渡して値を色に変換しています。
function getColor(d) { return d > 1000 ? '#800026' : d > 500 ? '#BD0026' : d > 200 ? '#E31A1C' : d > 100 ? '#FC4E2A' : d > 50 ? '#FD8D3C' : d > 20 ? '#FEB24C' : d > 10 ? '#FED976' : '#FFEDA0'; } function style(feature) { return { fillColor: getColor(feature.properties.density), weight: 2, opacity: 1, color: 'white', dashArray: '3', fillOpacity: 0.7 }; } L.geoJson(statesData, {style: style}).addTo(mymap);インタラクションを追加
ポリゴン上にマウスオーバーしたときのハイライト、マウスを外したときに元に戻す、クリックしたらズームする、という3つのインタラクションを追加します。それぞれstyleを変更するfunctionを書いておいてイベント発生時にonEachFeatureに呼び出してもらいます。
function highlightFeature(e) { var layer = e.target; layer.setStyle({ weight: 5, color: '#666', dashArray: '', fillOpacity: 0.7 }); if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) { layer.bringToFront(); } } function resetHighlight(e) { geojson.resetStyle(e.target); } function zoomToFeature(e) { mymap.fitBounds(e.target.getBounds()); } function onEachFeature(feature, layer) { layer.on({ mouseover: highlightFeature, mouseout: resetHighlight, click: zoomToFeature }); } geojson = L.geoJson(statesData, { style: style, onEachFeature: onEachFeature }).addTo(mymap);カスタムインフォを表示する
地図右上に表題とハイライトしている州の名前と人口密度を表示します
var info = L.control(); info.onAdd = function (mymap) { this._div = L.DomUtil.create('div', 'info'); // create a div with a class "info" this.update(); return this._div; }; // method that we will use to update the control based on feature properties passed info.update = function (props) { this._div.innerHTML = '<h4>US Population Density</h4>' + (props ? '<b>' + props.name + '</b><br />' + props.density + ' people / mi<sup>2</sup>' : 'Hover over a state'); }; info.addTo(mymap);内容の更新はマウスオーバーの際に呼ばれる関数にやってもらいます
function highlightFeature(e) { ... info.update(layer.feature.properties); } function resetHighlight(e) { ... info.update(); }情報欄の背景を白くしてフォントを設定するCSSを追加します
.info { padding: 6px 8px; font: 14px/16px Arial, Helvetica, sans-serif; background: white; background: rgba(255,255,255,0.8); box-shadow: 0 0 15px rgba(0,0,0,0.2); border-radius: 5px; } .info h4 { margin: 0 0 5px; color: #777; }色の目安を表示する
var legend = L.control({position: 'bottomright'}); legend.onAdd = function (mymap) { var div = L.DomUtil.create('div', 'info legend'), grades = [0, 10, 20, 50, 100, 200, 500, 1000], labels = []; // loop through our density intervals and generate a label with a colored square for each interval for (var i = 0; i < grades.length; i++) { div.innerHTML += '<i style="background:' + getColor(grades[i] + 1) + '"></i> ' + grades[i] + (grades[i + 1] ? '–' + grades[i + 1] + '<br>' : '+'); } return div; }; legend.addTo(mymap);CSSに追記します
.legend { line-height: 18px; color: #555; } .legend i { width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.7; }
- 投稿日:2020-10-23T19:40:43+09:00
sign in with apple JS を使用して日本語のボタン生成やボタンデザインのカスタマイズ方法
Sign in with Apple JS を使用して日本語のボタン生成やボタンデザインのカスタマイズ方法
みなさんAppleは好きですか
僕は嫌いです。Sign in with Apple JS の使用方法
ドキュメントに基本的な使い方が記載されてます。
公式から転載
<html> <head> <meta name="appleid-signin-client-id" content="[CLIENT_ID]"> <meta name="appleid-signin-scope" content="[SCOPES]"> <meta name="appleid-signin-redirect-uri" content="[REDIRECT_URI]"> <meta name="appleid-signin-state" content="[STATE]"> </head> <style> .signin-button { width: 210px; height: 40px; } </style> <body> <div id="appleid-signin" class="signin-button" data-color="black" data-border="true" data-type="sign in"></div> <script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script> </body> </html>これでボタンが出ます。
ですが、metaを使用したくない場合は以下のようなjsファイルを作成し、htmlに読み込ませると表示されます。ダミーデータでも表示されます(ちょろい)
APIとjsの読み込み順に気をつけてください。AppleID.auth.init({ clientId : "xxx", scope : "xxx", redirectURI: "xxx", state : "xxx" });基本は以上ですが、他言語のAPIについてやサインアップボタンはどうやって表示するのか一切記載されていないのでどこを見ればいいのかまとめておきます。
日本語API
以下が日本語のAPIです。
https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/ja_JP/appleid.auth.jsなので読み込むjsを変更すれば日本語になります。
<!--英語--> <script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script> <!--日本語--> <script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/ja_JP/appleid.auth.js"></script>ボタンデザインのカスタマイズ
こちらのボタン生成のページが用意されています。はい。ドキュメントには記載されていないページですね。
ちなみにapple公式のページです。ボタン生成
https://appleid.apple.com/signinwithapple/button#center-align-button-sectionこちらのページの「Download」を押すと今表示されているボタンのPNGファイルがダウンロードされます。
PNGでボタンを作りたい場合はダウンロードした画像はボタンデザインのガイドラインに沿ってるのでそのまま使用して問題ないです。jsを使用する場合は表示されているボタンの下に
タグのコードが表示されているのでそれをそのままhtmlに載せると同じように表示されます。<div id="appleid-signin" data-mode="center-align" data-type="sign-in" data-color="black" data-border="false" data-border-radius="15" data-width="200" data-height="32" ></div>なぐり書きですが、そこまで難しくはないと思います
Apple製品は嫌いではないですが、仕事で Sign in with Apple を調査・使用することになりドキュメントを舐めまわしてましたが、ドキュメントに記載されていないことが多々あったので嫌いです。
デザインの規約も多すぎです。少しは妥協できんのか…
- 投稿日:2020-10-23T18:30:32+09:00
『Darkmode.js』『Bootstrap』でお手軽ダークモード
Darkmode.js
『Darkmode.js』でダークモード入れてみました。
Sample
See the Pen vYKgEVE by sarap422 (@sarap422) on CodePen.
head
<head> <!-- Bootstrap CDN --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <!-- darkmode-js CSS --> <link rel="stylesheet" href="js/darkmode-js/darkmode-js.css"> </head>/body
<!-- 疑似要素 --> <span class="gluttony-Sword-bs badge"><i></i></span> <!-- jquery + Bootstrap CDN --> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <!-- darkmode-js + function(CDN + option設定) --> <script src="https://cdn.jsdelivr.net/npm/darkmode-js@1.3.4/lib/darkmode-js.min.js"></script> <script src="js/darkmode-js/darkmode-js-function.js"></script> </body>darkmode-js-function.js
// option設定 var options = { // 左端に置く bottom: '-17px', // default: '32px' right: 'unset', // default: '32px' left: '-17px', // default: 'unset'darkmode-js.css
.gluttony-Sword-bs i:after { display: inline-block; padding: 0.25em 0.5em; font-size: 90%; font-weight: 600; line-height: 1; text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: 0.4rem; content: "Light"; color: #212529; background: #ffc83d; } .darkmode--activated .gluttony-Sword-bs i:after { font-weight: normal; content: "Dark"; color: #fff; background: #757575; }要するに「jQuary」「Bootstrap」「darkmode-js」本体はCDN読み込み、
『darkmode-js-function.js』は、デフォルトだと右下で邪魔だったので
option設定で左下に動かして<script></script>は別で読み込み、そのままだと意味不明なので、疑似要素で「Light」「Dark」書きましたみたいな
お手軽ダークモードです。『Bootstrap Icons』さんや『Bootstrap Toggle』さんというのも
CDNで使えるみたいですので、そちらを使えばもう少し見栄えよくなるかもですね。Bootstrap Icons
https://icons.getbootstrap.com/icons/toggle-on/
Bootstrap Switch Button
https://gitbrent.github.io/bootstrap4-toggle/参考
- Darkmode.Jsを使う - Webクリエイターボックス
https://www.webcreatorbox.com/tech/dark-mode「prefers-color-scheme」ではないので、
スマホのユーザー設定は反映されなさそうですけど、「prefers-color-scheme」絡めると今度はページ読み込みのたびに、
『Darkmode.Js』がリセットされそうで、結構大変そうだったので今回はやめました。CSSのfilterプロパティとJavaScriptで手軽にダークモードに切り替える | Free Style
https://blanche-toile.com/web/css-filter-darkmodeメディアクエリ
@media (prefers-color-scheme: dark) { :root --dark-setting: darkmode--activated; }みたいにしたらいけそうな気もするのですけど、
たぶんこれからは、SCSSの$変数をメインで使いそうな気がしますので
とりあえずはこんな感じなのでした。
- 投稿日:2020-10-23T18:17:37+09:00
E2Eテスト 番外編 -シャープで画像連結-
前回の記事では、reg-cliを使った画像差分比較について紹介しました。
E2Eテストの始め方 番外編 - reg-cliで差分比較 -
そして今回は、TestCafeで撮影した複数枚のスクリーンショット画像を連結するためのsharpについて書いていきたいと思います。sharp とは
某電機メーカーが強すぎてググラビリティが残念なライブラリ。。。
同じようなライブラリとしてImageMagickやGraphicsMagickも有名ですが、ドキュメントによるとそれらより4倍~5倍速いそうです。
読み込みはJPEG,PNG,WebP,TIFF,GIF,SVGをサポート。
様々なサイズのJPEG,PNG,WebPに変換可能で、リサイズ以外に回転、抽出、合成、ガンマ補正、圧縮などの操作も可能となっています。
今回sharpを選んだ理由としてImageMagickらはしばらくメンテナンスされていない(sharpは執筆時点で10h前と活発に動いてる)ことと、処理の速さでsharpを採用しました。install
$ npm install sharpusage
convert.jsconst sharp = require('sharp');【ソース全文】
convert.jsconst fs = require('fs') const config = require('../config') const { imgPath, imgPc, imgSp } = config; //出力先フォルダの作成 const makeDir = () => { fs.mkdir('screenshots/convert', { recursive: true }, (err) => { if (err) throw err; }); } //sharp const convert = async (imagePaths, imageName) => { const imageAttrs = []; // 連結する画像の情報取得 const promises = []; const imagePromise = path => new Promise(async resolve => { const image = await sharp(path); let width = 0, height = 0; await image .metadata() .then(meta => ([width, height] = [meta.width, meta.height])); const buf = await image.toBuffer(); resolve({ width, height, buf }); }); imagePaths.forEach(path => promises.push(imagePromise(path))); await Promise.all(promises).then(values => { values.forEach(value => imageAttrs.push(value)); }); // outputする画像の設定 const outputImgWidth = imageAttrs.reduce((acc, cur) => acc + cur.width, 0); const outputImgHeight = Math.max(...imageAttrs.map(v => v.height)); let totalLeft = 0; const compositeParams = imageAttrs.map(image => { const left = totalLeft; totalLeft += image.width; return { input: image.buf, //合成場所 gravity: "northwest", left: left, top: 0 }; }); // 連結処理 sharp({ create: { width: outputImgWidth, height: outputImgHeight, channels: 4, background: { r: 255, g: 255, b: 255, alpha: 0 } } }) //合成 .composite(compositeParams) //圧縮 .png({ quality: 80, compressionLevel: 9 }) .toFile(`screenshots/convert/${imageName}.png`,(err, info)=>{ if(err){ throw err } console.log(info) }); } (async () => { await makeDir() convert(imgPc, 'pc') convert(imgSp, 'sp') })()解説
まず、出力先のフォルダを作成します。
フォルダがないと「どこに保存するのか分からないよ!」と怒られてしまうので、sharpを実行する前にフォルダを用意してあげます。convert.js//出力先フォルダの作成 const makeDir = () => { fs.mkdir('screenshots/convert/corporate', { recursive: true }, (err) => { if (err) throw err; }); }画像の情報取得や連結処理などのソースはこちらを参考にさせていただきました。
NodeJSの画像処理ライブラリ「sharp」を使って画像を連結する私は、連結したい画像がたくさんあるため
config.jsに画像パスの配列を記述し呼び出して使っています。
第一引数imagePathsに呼び出した画像パスの配列imgPcとimgSpを指定し、
第二引数のimageNameには出力される画像のファイル名を指定します。
(デフォルトのピクセル制限(268402689)を超えてしまうので複数枚に分けて出力しています)convert.jsconst config = require('../config') const { imgPath, imgPc, imgSp } = config; const convert = async (imagePaths, imageName) => { .toFile(`screenshots/convert/corporate/${imageName}.png`,(err, info)=>{ }); } (async () => { await convert(imgPc, 'pc') convert(imgSp, 'sp') })()config.jsconst imgDir = `screenshots/${year}${month}${date}/` exports.imgPc = [ `${imgDir}/top_pc.png`, `${imgDir}/company_pc.png`, `${imgDir}/company_access_pc.png`, ]; exports.imgSp = [ `${imgDir}/top.png`, `${imgDir}/company.png`, `${imgDir}/company_access.png`, ];圧縮
生成された画像の圧縮も設定可能です。
jpeg
convert.js.jpeg({ quality: 80 })
options.quality:整数1〜100,デフォルト80
options.compressionLevel:zlib圧縮レベル、0〜9(デフォルト9)png
convert.js.png({ quality: 100, compressionLevel: 9 })
options.quality:デフォルト100
options.compressionLevel:zlib圧縮レベル、0〜9(デフォルト9)webp
convert.js.png({ quality: 80, compressionLevel: 9 })
options.quality:整数1〜100,デフォルト80
options.lossless:可逆圧縮モード,デフォルトfalseこのほか、gifやtiff,heifも対応可能でオプションで細かい設定もできるので詳細は公式をご覧ください。
https://sharp.pixelplumbing.com/api-output実行
$ node e2e/convert.js完成!
横並びにがっちゃんこして出力してくれました!
各ページフルサイズのスクリーンショットを連結させると出力サイズがかなり大きくなるので圧縮率を大きくしたり、連結枚数を少なくして何枚かに分けるなど工夫が必要ですが良い感じにできたのではないかと思います
参考
NodeJSの画像処理ライブラリ「sharp」を使って画像を連結する
公式
SHARP Be Original
- 投稿日:2020-10-23T14:51:01+09:00
eslint の設定で import をきれいにする
はじめに
複数人で開発する際に「コーディングルールを揃えたい」というケースはよくあります。
各々が自由に開発していると、import の順番がバラバラになったりして、プルリクエストの際に本質的ではない変更が混在します。私が香港のスタートアップで開発していたときは、チームメンバー全員が VSCode を利用しており、settings.json に
"editor.codeActionsOnSave": { "source.organizeImports": true, },を入れることで、統一していました。
しかし、エディタ(は宗教問題なので)を統一したくないケースもあると思いますので、 eslint で出来ればベターだと思います。今回は、その方法をご紹介します。
実装
今回ご紹介するプラグインは
import,unused-importsです。
まずはプラグインをインストールします。yarn add -D eslint-plugin-import eslint-plugin-unused-imports次に
.eslint.yamlを下記のように変更します。# .eslint.yaml env: browser: true es2021: true extends: - 'plugin:prettier/recommended' - 'prettier/@typescript-eslint' - 'prettier/react' parser: '@typescript-eslint/parser' parserOptions: ecmaFeatures: jsx: true ecmaVersion: 12 sourceType: module plugins: - react - '@typescript-eslint' - import rules: sort-imports: 0 "import/order": - warn - groups: - builtin - external - internal alphabetize: order: asc筆者の環境では VSCode の設定に、
"editor.codeActionsOnSave": { "source.fixAll.eslint": true },が入っているので、ファイルを変更して保存した瞬間に import の順番が変わります。
修正前
修正後
しかし、このままでは不要な(利用していない)import が残っています。
これも自動で消したいです。その設定を入れていきます。
# .eslint.yaml ... plugins: ... - unused-imports rules: ... "@typescript-eslint/no-unused-vars": off unused-imports/no-unused-imports-ts: warn ...修正後
不要な import が消えました。
おわりに
複数人で開発する時に、コードフォーマッタや elinter で記述ルールを揃えると開発速度が一気に加速します。
今回紹介した方法よりも良い方法や、他にも記述改善するためのテクニックがあればぜひ教えてください。
- 投稿日:2020-10-23T14:27:37+09:00
playwrightのテスト結果を動画として残す方法
はじめに
e2eテスト界にキラ星のごとく登場したplaywright。
後発ツールなだけあって超絶クールですが、標準構成のみでテスト結果を動画として保存することができます。動画を出力するための設定
const context = await browser.newContext({ videosPath: 'videos/' // 動画を出力するフォルダ名 });テストコードに上記設定を追加するだけで、テスト結果を動画に残せます。
本当にこれだけでOKなので、世の中便利になったものだなと思います。
なお、公式サイトには、これ以外のやり方も書かれているので、
よろしければ参考にしてください、公式ドキュメント
https://playwright.dev/#version=v1.5.1&path=docs%2Fverification.md&q=videosサンプルコード
const { chromium } = require('playwright'); (async () => { const browser = await chromium.launch({ headless: false }); const context = await browser.newContext({ videosPath: 'videos/' }); // Open new page const page = await context.newPage(); // Go to https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8 await page.goto('https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8'); // Click input[name="search"] await page.click('input[name="search"]'); // Fill input[name="search"] await page.fill('input[name="search"]', 'ユニットテスト'); // Go to https://ja.wikipedia.org/wiki/%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88 await page.goto('https://ja.wikipedia.org/wiki/%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88'); // Go to https://ja.wikipedia.org/wiki/%E5%8D%98%E4%BD%93%E3%83%86%E3%82%B9%E3%83%88 await page.goto('https://ja.wikipedia.org/wiki/%E5%8D%98%E4%BD%93%E3%83%86%E3%82%B9%E3%83%88'); // Click text="契約による設計" await page.click('text="契約による設計"'); // assert.equal(page.url(), 'https://ja.wikipedia.org/wiki/%E5%A5%91%E7%B4%84%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0'); // Click text="ソフトウェア開発工程" await Promise.all([ page.waitForNavigation(/*{ url: 'https://ja.wikipedia.org/wiki/%E5%8D%98%E4%BD%93%E3%83%86%E3%82%B9%E3%83%88' }*/), page.click('text="ソフトウェア開発工程"') ]); // Close page await page.close(); // --------------------- await context.close(); await browser.close(); })();wikipediaのページを遷移するテストを書きました。
8行目でvideosPath: 'videos/'と書いていますが、これが動画出力の設定です。
テストを実行するとvideosフォルダ配下に、テスト動画が自動生成されます。
念のためにgithubにコードのフルセットを用意したので、
実際に動かしてみたい方はぜひお試しください。コードフルセット
https://github.com/kaidouji85/playwright-video-record
- 投稿日:2020-10-23T11:13:12+09:00
webpack 5にアップデート
webpackを ^4.44.0 -> ^5.0.4にマイグレーションしましたので、
その際の作業の覚書です。migrationの仕方
https://webpack.js.org/migrate/5/
基本的に上記の公式を読みつつ、エラーが出たらその都度エラー文を元に対応していく形で進めました。
元々のwebpackの設定にもよって対応も変わってくるかと思いますが、
主要な変更箇所と実際に私が変更した箇所を紹介しておきます。loaderとuseの厳格化
【参考】 https://blog.hiroppy.me/entry/webpack5
rules.loaderとrules.useの使用ルールが厳格になったことで、
環境によってはエラーとなる可能性があります。
optionsを使用しない場合はuse、
optionsを使用する場合はloaderとなるように設定を見直しましょう。optionsのアップデート
今回のアップグレードでいくつかのoptionsはoutdatedとなり、他のものに変える必要があります。
(以下、公式から)
- optimization.hashedModuleIds: true ↦ optimization.moduleIds: 'hashed'
- optimization.namedChunks: true ↦ optimization.chunkIds: 'named'- ・optimization.namedModules: true ↦ optimization.moduleIds: 'named'
- NamedModulesPlugin ↦ optimization.moduleIds: 'named'
- NamedChunksPlugin ↦ optimization.chunkIds: 'named'
- HashedModulesPlugin ↦ optimization.moduleIds: 'hashed'
- optimization.noEmitOnErrors: false ↦ optimization.emitOnErrors: true
- optimization.occurrenceOrder: true ↦ optimization: { chunkIds: 'total-size', moduleIds: 'size' }
- optimization.splitChunks.cacheGroups.vendors ↦ optimization.splitChunks.cacheGroups.defaultVendors
- Compilation.entries ↦ Compilation.entryDependencies
- serve ↦ serve is removed in favor of DevServer
私の場合は
NamedModulesPluginを使用していたため、その設定を見直しました。
これはHMR(ホットモジュールリロード)のためのoptionsですが、指示通りoptimizationの設定に変更しました。webpack.config.jsplugins: [ - new webpack.NamedModulesPlugin(), ... ], optimization: { + moduleIds: 'named', + ... }しかし、その後の公式の文にこんなものが。
Consider removing optimization.moduleIds and optimization.chunkIds from your webpack configuration. The defaults could be better, because they support long term caching in production mode and debugging in development mode.
(雑訳)
optimize.moduleIdsとoptimization.chunkIdsの設定を削除することを検討してください。
デフォルトだとproductionモードでは長めのキャッシュを、developmentモードではデバックをサポートしますよ。?
ということで、色々検討した結果
moduleIdsも外すことにしています...webpack.config.jsoptimization: { - moduleIds: 'named', ... }その他細かい設定
When using [hash] placeholder in webpack configuration, consider changing it to [contenthash]. It is not the same, but proven to be more effective.
If you are using Yarn's PnP and the pnp-webpack-plugin, we have good news: it is supported by default now. You have to remove it from the configuration.
If you are using IgnorePlugin with a regular expression as argument, it takes an options object now: new IgnorePlugin({ resourceRegExp: /regExp/ }).
If you are using node.something: 'empty' replace it with resolve.fallback.something: false.file-loader
色々ありますが、 私は [hash]を
file-loaderで使用していたので
[contenthash]に変える作業が発生しました。
また、file-loaderはクエリでのoption指定からoptionsでの指定にかわったようなので、こちらも調整します。webpack.config.jsmodule: { rules: [ { ... test: /\.(jpe?g|png|gif|svg)$/i, - loader: 'file-loader?name=css/img/[name].[ext]?[hash]', + loader: 'file-loader', + options: { + name: 'css/img/[name].[ext]?[contenthash]', + }, ...IgnorePlugin
IgnorePluginの指定方法も変わっていますので、
設定されている場合はそちらの記述も変更します。
私はmoment.jsのために使用していたので記述を変更しました。(そもそもmoment.jsやめないとなあ...)webpack.config.js- new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), + new webpack.IgnorePlugin( + { + resourceRegExp: /^\.\/locale$/, + contextRegExp: /moment$/, + }, + ),古いパッケージやエコシステムの見直しを
私の環境では
optimize-css-assets-webpack-pluginが原因でエラーとなっている部分がありました。
"optimize-css-assets-webpack-plugin": "^3.2.1", だったので、
"optimize-css-assets-webpack-plugin": "^5.0.4" に変更したところエラーが解消されました。
この辺も一通り見直さないといけないですね。俺たちの戦いはこれからだ!
上記作業で一通り動作の問題なさそうなものの、
deprecationWarningがめっちゃ出てます。
こちらも見直さなければ....
余裕があればその解消の作業も追記していきます。最後に、公式手順にlevel別stepがのっていましたのでざっくり意訳しておきます。
公式ではLevel7までありますが、最低でも3 or4レベルまで繰り返してとのことです。Level1. スキーマバリデーションエラー。BREAKING CHANGE: といったエラーや、代替オプションにしたがってoptionを変更すること! Level2. webpackエラー。エラーメッセージをよめ!エラーメッセージが全て教えてくれる。 Level3. ビルドエラー。BREAKING CHANGE: といったエラーが出ているはず。 読め。 Level4. ビルドワーニング。 ワーニングメッセージは改善点を示してくれます。
- 投稿日:2020-10-23T09:13:17+09:00
アコーディオンパネル使い方
アコーディオンパネル
html<dl class="accordion"> <dt>A</dt> <dd class="dda">ここにAの内容が入る</dd> <dt>B</dt> <dd class="ddb">ここにBの内容が入る</dd> <dt>C</dt> <dd class="ddc">ここにCの内容が入る</dd> </dl>css.accordion{ width: 500px; margin: 50px auto; text-align: center; } dt { padding: 16px; background: #2894f0; border: 2px solid #2894f0; color: #ffffff; font-size: 24px; } dd { height: 100px; padding: 16px; border: 2px solid #2894f0; border-top: 0; }js$(function() { $('dd').hide(); $('dt').on('click',function() { $('.dda').slideDown(400); }); });
- 投稿日:2020-10-23T06:01:22+09:00
引数を使って抽象度のあるメソッドを作る
下記の2つの動画とコードを参考にしています。
https://github.com/seito-developer/js-tutorial/blob/master/index.js
https://www.youtube.com/watch?v=QCjFPSO96RUまずそこまで抽象度のないメソッド
const inoki = ['いーち','にーい','さーん','ダーー!!']; const test = () => { //ここに実行したい命令を書く if(inoki.length > 3){ console.log('ボンバイエ!'); } else { console.log('ボンバ...!'); } return false }; test(); // ボンバイエ!抽象度を上げたメソッド
const inoki = ['いーち','にーい','さーん','ダーー!!']; const test = (num) => { //ここに実行したい命令を書く if(inoki.length > num){ console.log('ボンバイエ!'); } else { console.log('ボンバ...!'); } return false }; test(2); // ボンバイエ! test(1000); // ボンバ...!
- 投稿日:2020-10-23T00:39:29+09:00
WEB開発の基礎知識(フロントエンド)
はじめに
WEB開発を行ううえでの、超初心者エンジニアが最初に覚えると良い基礎知識をまとめました。
フロントエンド開発をこれから勉強したい!という方向けの内容となっております。実際の開発知識を身につける前に、
○この技術の誕生に至る背景はどのようなものか?
○普段当たり前に使用しているモノはどういう仕組みで動いているのか?
を頭に入れておく事はきっと役に立つはずです。「ふーん、なるほどね」くらいで、そこら辺のフリーペーパーとでも思って見ていただければと思います。
※ 私自身も勉強中の身でして、初心者に一本毛が生えた位のレベルですので、間違い等お気づきになられましたら
お手数ですがご指摘頂けると幸いです。随時追記、修正していきますので悪しからず。WEBの基礎知識
よく耳にする用語集
WWW(World Wide Web)
インターネット上の様々なコンテンツをインタラクティブ(相互的)に
閲覧するための技術の総称です。
単にインターネットという表現そのものを意味する場合もあるそうです。
いろいろなURLの中に入っているwwwも、こちらが由来です
(例)https://www.google.com/
ちなみに一番最初のWEBサイトが公開されたのは1991年と言われています。
意外と最近だったんですね。W3C(World Wide Web Consortium)
WWWの様々な技術を標準化、推進する団体です。
つまり、「WEBでこういうことがしたかったら、こういう方法で開発するのがオススメです!」
というルールみたいなものを定義している人達。
WWWの開発者でもある、ティム・バーナーズ=リーさんが1994年に設立しました。ハイパーテキスト
ハイパーリンクと呼ばれる、他のWEBページに遷移することができる機能が用いられた
ドキュメントのこと。後述しますがこれを書くための特有の言語があります。クライアント
営業などのお仕事ではお客様みたいなニュアンスですが、WEBの世界では
私達が直接使うPC環境、WEBブラウザのことです。
そのためクライアントと言う言葉は時々、フロントと置き換えられます。
クライアント側で、WEBを閲覧するためのインターフェースを提供するのがブラウザになります。インターフェース
コンピュータで、異なる機器・装置のあいだを接続して、交信や制御を可能にする装置やソフトウェア。サーバー
通信をするなにか。と思いがちですが、本当は、色々なデータを保存していて
クライアントからの要求に応えて何らかの応答を返す媒体です。
因みにこの要求をリクエスト、応答をレスポンスと呼びます。
サーバーからのレスポンスにはHTMLやCSSのファイルが含まれており、
それを受け取ったWEBブラウザがファイルを解析し、WEBページとして表示する仕組みになっています。HTML(Hyper Text Markup Language)
ハイパーテキスト(WEBページ)をマークアップ(記述)するための言語です。
私達が普段見ているWEBページも、ほとんどこのHTMLで記述されています。
(厳密にはHTMLの他にも色々な技術が合わさって作成されています。)
また、マークアップは訳すと「マークをつける」ということになりますが
WEBページ内の様々なコンテンツに対して印を付け、ブラウザが認識し易くする。
といった感じになります。CSS(Cascading Style Sheets)
WEBページにスタイルをあてる(装飾する)ための言語です。
検索してヒットするWEBページのほとんどは、HTMLにプラスしてこのCSSも
使われている場合がほぼ100%と言って良いでしょう。
因みにHTMLとCSSを使ってWEBページを作成する人を
「マークアップエンジニア」と呼んだりします。Javascript
おもにWEBで活用される、フロントエンド(見た目部分)に関与しているプログラミング言語です。
先述したHTMLやCSSはプログラミング言語ではないようです。
HTMLとCSSだけでは毎回同じサイトしか表示できませんが、
Javascriptを用いると次のようなことができたりします。
・日付や時刻によって、サイトのヘッダー内容の表示を変える
・ボタンAが押されたら○○をする。というようなプログラムをWEBページに与えられる
・ブログの投稿のお気に入り数によって、ランキング一覧を毎回更新して表示する
Javascriptを使うと、サイトを「動的」にすることができます。
HTMLやCSSのみのページは「静的」です。
フロントエンドの技術進歩は凄まじく、今はJavascriptで出来ることが沢山あります。そもそもプログラミング言語とは
コンピュータ内で人間の頭脳にあたるパーツである、CPU(中央処理装置)が読める複雑な、
機械語というものを人間が理解しやすい様に開発された言語。因みにこのプログラミング言語を機械語に変換(コンパイル)する必要があるが、2パターンの方式が存在する。
・コンパイラ方式 (windowsやmacアプリの開発言語に多い)
あらかじめコンパイルをしておいてからプログラムを実行
→プログラムを高速に処理できるが、プログラム修正のたびにコンパイルの作業が必要・インタプリタ方式 (WEBの開発言語に多い)
プログラムの実行と同時にコンパイルを実行
→コンパイラ形式より実行速度は落ちるが、修正や変更のたびにコンパイルを行わずにすむフロントエンドって何?
ユーザの目に見える箇所全般のことを指します。
つまりフロントエンド開発とは、見た目の改善、見た目を作成する仕組みの改善を行い、
利用者に安心かつ最適なユーザーインターフェース(UI)を提供すること。だと思っています。対立する用語はサーバーサイドといい、こちらはユーザが直接見ない部分である
サーバーに寄った部分のことを指します。
(ざっくりし過ぎですが、イメージ的にはデータベースとか、インフラとか、APIとか
ちょっとお固い感じ。)因みに...フロントエンドのお仕事で最近よく聞く
- WEB制作
- フロントエンドエンジニア
これらは何が違うのかというと。
WEB制作...
HTML/CSS/JQuery(Javascriptのライブラリです)/
WordPress(CMSといってウェブサイトを簡単につくれるシステムです)
→これらを用いて静的なページをつくる。
フロントエンドエンジニア
HTML/CSS/Javascript/JSフレームワーク/..他大量の技術
→を用いて、サーバーサイドのエンジニアと連携して
WEB制作よりもしっかりした動的なサイトやアプリケーションをつくる。といった感じです。
開発環境について
開発環境とは、その名の通りWEBの技術を開発する上での作業場所という意味になります。
サーバーで実行されるサーバーサイドの技術は、もちろんそのサーバーを準備する必要が
あり、設定や手順が厄介なものがありますが、フロントエンドの実行する場所はブラウザです。
ブラウザと文字が書けるエディタがあればプラクティスが可能です。
- 投稿日:2020-10-23T00:16:03+09:00
誤った情報をpushしてしまった時の対処法
この記事で分かること
- 誤った情報をpushしてしまった
- 誤った情報をcommitしてしまった
誤った情報をpushしてしまった
pushした情報をローカルリポジトリに戻すことはできません。したがって、pushする前に作業をしているブランチは正しいのかどうか、必ず確認するようにしましょう。
それでも間違ってpushしてしまうことがあります。ローカルリポジトリにその情報は戻せないものの、リモートリポジトリにある誤ったcommit情報は取り消すことができますcommitを取り消す方法
commitを取り消すためには、revertと呼ばれる技術を用います
間違ってpushしたcommitを取り消すことができます。commitを削除するのではなく、「指定するcommitを取り消すためのcommit」を追加で行います。
revertはcommitされた変更と逆になる変更を追加することで、commitを取り消します誤った情報をcommitしてしまった
現場からは以上です!
- 投稿日:2020-10-23T00:07:21+09:00
球上で互いに距離をおいてみた
See the Pen MovingPointsToKeepDistanceOnTheSphereUsingThreeJs by kob58im (@kob58im) on CodePen.
やっていること:球上で、互いの点間の距離の最小値が増加する側に点を徐々に移動させています。
やりたかったこと:正多面体とかの頂点に落ち着かないかなー・・・と思ったけど局所最適解ちっくなとこに落ち着いてしまうので無理でした。
コロナ禍っぽいタイトルになったのは偶然です。。
参考サイト































































