20201023のJavaScriptに関する記事は25件です。

GraphQLとVueを使ってミニアプリを作る

はじめに

本記事ではVue Apolloを利用して、GraphQLとVueアプリを接続した後に、CRUD機能並びにサブスクリプションの実装を目指す内容となります。

完成イメージ
スクリーンショット 2020-10-23 23.00.35.png

大まかな概要としては、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.js
const posts = [{
    id: '1',
    title: 'こころ',
    author: '夏目漱石'
}, {
    id: '2',
    title: '舞姫',
    author: '森鴎外'
}, {
    id: '3',
    title: '羅生門',
    author: '芥川龍之介'
}]

const db = {
    posts,
}

module.exports  = db;

Query,Mutation,Subscriptionに関する処理をまとめたファイルを順番に用意していきます。

Query.js
const 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.js
const 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.js
const Subscription = {
    post: {
        subscribe(parent, args, { pubsub }, info) {
            return pubsub.asyncIterator('post')
        }
    }
}

module.exports = Subscription

サブスクリプションのファイルです。
pubsub.asyncIteratorでサブスクリプションのイベントを非同期でリッスンします。

説明の関係で最後になりましたが、
スキーマの定義とサーバー起動のファイルになります。

index.js
const  {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/

スクリーンショット 2020-10-23 21.02.27.png

Vueプロジェクト内の設定

続いてVueでGraphQLを扱うために、Vue Apolloをインストールしていきます。
本記事においては設定は全てデフォルトで進みます(色々聞かれますが「No」と答えてます)

$ vue add apollo

見た目をリッチにするために、Vuetifyもインストールします。

$ vue add vuetify

以上にて、プロジェクトの設定は完了です。
Vue-ApolloとVuetifyの設定はmain.jsに記述があります。

main.js
import 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.js
import 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 serve

http://localhost:8080/

Vueを利用して模擬データベースの値を表示することが出来ました!

スクリーンショット 2020-10-23 22.31.34.png

ミューテーションの実装

続いてミューテーションの実装を行います。
こちらの章を終えると①書き込み、②更新、③削除 が出来るようになります。

先ずはファイルを作りましょう。

$ touch src/graphql/post-mutation.js

ミューテーションを書いていきます。

post-mutation.js
import 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処理が出来るようになりました!!

ezgif-4-bad1856686d7.gif

サブスクリプションの実装

最後にサブスクリプションの実装を行います。
こちらの章を終えるとリアルタイムでの書き込みの共有が出来るようになります。
実際のアプリケーションでは通知機能を作る時などに使う部分となります。

今回もファイルを作ります。

$ touch src/graphql/post-subscription.js

サブスクリプションを書きます。

post-subscription.js
import 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つ用意します。

subscriptionVue.gif

こちらも上手くいきました!!!

おわりに

以上、今回はGraphQLをVueで扱ってみました。
今回の内容を元に仕様に合わせてクエリやスキーマの数を増やすことで、発展的なアプリケーションを作ることが出来るかと思います。
次回はGraphQLとデータベースの接続について記事にしたいと思います!

それでは、また?

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

HTMLを読み込んでからJSが読み込まれるようにする方法

はじめに

 少し苦手意識のあるJSでエラーが出たので、忘れないために…

コードは合っているのに、エラーが出た

JSとHTMLは切っても切れない関係。
HTMLを読み込み、JSが読み込まれることで、JSがしっかりと動いてくれる。
そう、逆だと動かない。
JSを読み込み、HTMLを後から読み込むと、エラーが出る。考えればわかることだが、JSではgetElementByIdなどで、HTMLに記述されているid名を読み出す。
HTMLを読み込んでから、JSが読み込まれるように、次の記述をJSにする。

window.addEventListener('load', () => {
 //処理をここに記述
});

コードの意味は、「ページをloadしたら、イベントを発火させる」
基本これを忘れずに、最初に記述しておく。

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

Laravel8.9.0 + Vue.jsでaxiosを使ったときにthenとcatchの両方を通るときの対処法

@babel/runtimeをインストールする

npm install --save @babel/runtime
npm install --save-dev @babel/plugin-transform-runtime
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

5.TMDB リスト いいね 通知

はじめに

1.アプリケーション紹介
2.認証機能
3.TMDB api fetch
4.auto-suggestion検索フォーム
5.ジャンルフィルターとページ移動
6.リスト いいね 通知

今回はログイン後の機能でリストといいねと公開前の映画を通知希望のリストに追加するとヘッダーで通知するようにします。
コードに関しては、githubにて後悔しておりますので、こちらからどうぞ!

リスト

リスト.gif

こんな感じでユーザーリストを作理、リストに追加できるようにします。

まずはじめにfirestoreないから、、、

firebase内のデータベース設計

ユーザー1人に、folderコレクションを作ります。
そこにはユーザーとは別のコレクションのfolderに映画をセットしていくようになります。

下記がユーザーの中のfolderコレクションなります。こちらのidからfolderコレクションと紐付けしていきます。
スクリーンショット 2020-10-23 19.20.39.png

そしてこちらがユーザーとは別のfolderコレクションの中身で実際にはこちらに映画をセットしていきます。
スクリーンショット 2020-10-23 19.22.09.png

リスト作成

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コレクションを用意、こちらも同様に初ログインからデフォルトで入っているリストになります。
そして未公開の場合にだけこちらのリストに追加できるようにします。
notification.gif
これに関しては、映画の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公式のアプリを大いに似せていただきました。これから勉強を重ねていきたいと思っております。

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

6.TMDB リスト いいね 通知

はじめに

1.アプリケーション紹介
2.認証機能
3.TMDB api fetch
4.auto-suggestion検索フォーム
5.ジャンルフィルターとページ移動
6.リスト いいね 通知

今回はログイン後の機能でリストといいねと公開前の映画を通知希望のリストに追加するとヘッダーで通知するようにします。
コードに関しては、githubにて後悔しておりますので、こちらからどうぞ!

リスト

リスト.gif

こんな感じでユーザーリストを作理、リストに追加できるようにします。

まずはじめにfirestoreないから、、、

firebase内のデータベース設計

ユーザー1人に、folderコレクションを作ります。
そこにはユーザーとは別のコレクションのfolderに映画をセットしていくようになります。

下記がユーザーの中のfolderコレクションなります。こちらのidからfolderコレクションと紐付けしていきます。
スクリーンショット 2020-10-23 19.20.39.png

そしてこちらがユーザーとは別のfolderコレクションの中身で実際にはこちらに映画をセットしていきます。
スクリーンショット 2020-10-23 19.22.09.png

リスト作成

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コレクションを用意、こちらも同様に初ログインからデフォルトで入っているリストになります。
そして未公開の場合にだけこちらのリストに追加できるようにします。
notification.gif
これに関しては、映画の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公式のアプリを大いに似せていただきました。これから勉強を重ねていきたいと思っております。

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

4.TMDB ジャンルフィルターとページ移動

はじめに

1.アプリケーション紹介
2.認証機能
3.TMDB api fetch
4.ジャンルフィルターとページ移動
5.リスト いいね 通知

アプリの概要など他にもこのアプリケーションについての記事を載せておりますのでそちらの方がみたい方はそちらをどうぞ!
コードもgithubにてのせておりますので、こちらからどうぞ!
今回は、ジャンルを選択したときにフィルターにかけてapiをたたいて映画の取得を行います。

そしてページ遷移ができるコードも載せていきたい思っております。

ジャンルフィルター

タイトルなし.gif

こんな感じのジャンルフィルターを作っていきます。

//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が変わるようにしています。

ページ遷移

タイトルなし.gif
こちらはページ遷移です。

//次にページをセット
   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を戻ったり、次へ進んだりできるようになる。

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

5.TMDB ジャンルフィルターとページ移動

はじめに

1.アプリケーション紹介
2.認証機能
3.TMDB api fetch
4.auto-suggestion検索フォーム
5.ジャンルフィルターとページ移動
6.リスト いいね 通知

アプリの概要など他にもこのアプリケーションについての記事を載せておりますのでそちらの方がみたい方はそちらをどうぞ!
コードもgithubにてのせておりますので、こちらからどうぞ!
今回は、ジャンルを選択したときにフィルターにかけてapiをたたいて映画の取得を行います。

そしてページ遷移ができるコードも載せていきたい思っております。

ジャンルフィルター

タイトルなし.gif

こんな感じのジャンルフィルターを作っていきます。

//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が変わるようにしています。

ページ遷移

タイトルなし.gif
こちらはページ遷移です。

//次にページをセット
   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を戻ったり、次へ進んだりできるようになる。

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

【初心者でもわかる】select要素を使わず、divでselectみたいな動きを作る

どうも7noteです。select要素にcssが使いにくいので、他の方法を考えてみました。

select要素にはCSSが調整難しいため、自由に調整できるdiv要素でできる方法を書いていきます。
また、PCとスマホでselect要素風とそうじゃない動きに切り分けるなどが必要な時にも使えると思います。

sample.gif

書き方

※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.css
ul {
  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のちょいテク詰め合わせ

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

3.TMDB データfetch

はじめに

1.アプリケーション紹介
2.認証機能
3.TMDB api fetch
4.auto-suggestion検索フォーム
5.ジャンルフィルターとページ移動
6.リスト いいね 通知

今回は実際にapi叩く処理を載せていきます。
tmdbのapiは充実しているのでより詳しく映画の詳細の表示やフィルタリングができました。
どんなアプケーションかは、1回目の記事にて載せておりますのでみていただけると幸いです。

コードがあればいいという方は、こちらからどうぞ!

概要

今回は、新作公開(upcoming)、人気(popular)、公開中(now_playing)、高評価(top_rated)を映画のリストを表示できるようにします。
それごとにジャンルのフィルターをかけられるようにしています。

ジャンル.gif

映画リスト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も続けて叩けるようになっているので、それをつかって
より詳しく映画の詳細を表示できるようになっています。
映画詳細.gif
映画の詳細はこんな感じです!

俳優詳細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';

俳優詳細のページに出演作品も掲載します。
スクリーンショット 2020-10-22 22.43.54.png

こんな感じで表示します。デザインはTMDBに似せております。

ページのデザインやなどはgithubのコードにて確認できます。
ここでは省かせていただきます。

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

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を叩いて映画を取得できるようにします。
タイトルなし.gif

人気だけでなく、公開中、高評価、新作公開のapiもあったので、それごとにapiに上記のようなジャンルフィルターに掛けられるようにしています。
スクリーンショット 2020-10-22 20.33.26.png

つぎに認証機能にログインするといいねとリスト作成、追加、削除ができます。
リスト.gif

映画未公開の場合は、一週間前から通知されるようにしました。
notification.gif

映画の検索フォームに入力すると1文字ごとにapiを叩き、検索結果が出力されるようになっています。
suggestion.gif

以上が大体のアプリ紹介になります。

使ったパッケージ

"@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つ記事を上げていきますので、みていただけると幸いです。

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

チームで出欠管理システムを作りました

社内チームで今年一年の成果物として出欠管理システムを作成しました!!
この記事ではその概要を紹介します。

プロジェクト概要

成果物として何を作成するかチームで話し合い、社内全体で利用でき作業効率化につながるシステムについて考えました。
社内行事の出欠管理がExcel管理されており、作業負荷がかかる状態だったため、この改善としてQR認証を利用した出欠管理システムを作ることになりました。

システム運用イメージ

  1. 管理者がユーザーのQRコードを発行
    image.png

  2. 管理者がユーザーへQRコードを送付(Slack・メール等)
    image.png

  3. 開催者がイベント開催日を登録

  4. ユーザーがイベント参加時にQRコードをかざす
    (開催者はQR認証画面を表示しておく)
    image.png

  5. 開催者がイベントの出席状況を確認する

システムの使い方

QRコード発行

  • 「QRコードを生成する」を選択します。
    01 - コピー.PNG

  • ユーザーコード・ユーザー名を入力します。
    02.PNG

  • QRコードが発行されます。
    管理者は各ユーザーへQRコードを通知します。
    03.PNG

イベント登録/QRコード読み取り

  • 「QRコードを読み取る」を選択します。
    01.PNG

  • イベント名・イベント日付を入力します。
    05.PNG

  • 登録したイベントを選択します。
    06.PNG

  • QRコード読み取り画面が表示されるので、QRコードを認証します。
    07.PNG

  • QRコードの認証に成功するとこのような画面になります。
    08.PNG

イベント履歴確認

  • 「イベント履歴を見る」を選択します。
    09.PNG

  • 登録済みのイベントが表示されます。
    10.PNG

  • イベント参加者が確認できます。
    11.PNG

活用技術

メンバーの各々が得意な分野を用いて作成されています。

  • 実行環境
    • 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を発行することで保存するようにしました。

構成図.jpg

開発期間

主担当3人で日常業務の片手間で約3か月ほどかかりました。
実際の作業時間はもっと短かったです。

苦労した点

  • 担当者3人で作った成果物の結合
    インフラ面で統一した開発環境を用意せず進めたため、実際に結合した際にCORSの考慮漏れ等でwebアプリがうまく動作しませんでした。
    躓いた点はIssueに整理して今後のチーム開発では気を付けていきます。
  • クライアントからAPIへのアクセス
    Web画面からAPIにリクエストする際、クライアントからリクエストを飛ぶことを意識していなかったため、ファイアウォールやDNS・LB・Nginxの設定に苦労しました。
    今後システム構成を考える際は意識していきたいと思います。

今後の展開

  • 管理画面へのログイン認証機能の実装
    実装工数が足りなかったため実装できませんでした。
    Amazon Cognitoを利用した実装を検討中です。
  • システム基盤をAWSへ移行
    前述のAWS Cognito実装などAWSサービスの勉強も今後チームで実施していきたいと考えています。
    併せて、基盤移行も検討中です。
  • 各社内イベントでの運用
    コロナの影響で実際に人が集まる機会がなく、テストが不十分です。
    データ量や負荷など分からない点が多いので、今後活用してもらっていきたいと考えています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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

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

predicateとは?表示されたときの対応方法

predicateとは?表示されたときの考え方

Google Chromeのconsoleで関数を入力した時に表示されるpredicateについて。

例えば、以下のようにfindIndex関数を入力したときに表示される。

image.png

vscodeだと以下。

image.png

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)

#1

x==3かどうかを判定している。Yesならtrue、Noならfalseとなる。


アロー関数 ((x)=> x==3)

カッコ内の式はアロー関数。functionで書く関数を簡易化したもの。

xは任意の値。findIndexの場合、オブジェクトで指定した配列の要素が一つ一つ入る。(for文で回している状態と同じ)

このxが3と等しいかを判断してる。

アロー関数を使わない場合
a.findIndex(function(x){
    return (x==3)
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 &copy; <a href="https://openstreetmap.org">OpenStreetMap</a>',
    maxZoom: 18}).addTo(mymap);

</script>

image.png

2. 要素を足す

マーカーを置く

var marker = L.marker([37.85, 138.8]).addTo(mymap);

image.png

サークルを描く

  var circle = L.circle([37.85, 139.1], {
    color: 'red',
    fillColor: '#f03',
    fillOpacity: 0.5,
    radius: 6000
  }).addTo(mymap);

image.png

ポリゴンを描く

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);

image.png

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.");

image.png

マップ上の座標に付ける

var popup = L.popup()
  .setLatLng([37.4, 138.9])
  .setContent("I am a standalone popup.")
  .openOn(mymap);

image.png

イベントに対応して付ける

以下のようにクリックイベントが発生したらポップアップを呼ぶこともできます

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);

image.png

4. GeoJSONを使う

ポリゴン座標をJSONでまとめて渡すことも出来ます。
GeoJSONという書式があって、その通りに書いて渡すだけです。
https://leafletjs.com/examples/geojson/

GeoJSON Specification (RFC 7946)
https://tools.ietf.org/html/rfc7946

var 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);

image.png

色を指定する

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);

image.png

5. コロプレス図を作る

地図にヒートマップを表示するアレです
image.png
手順は以下のチュートリアルのものを参照しています
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 &copy; <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>

image.png

色をつける

色や不透明度などを変更するには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);

image.png

インタラクションを追加

ポリゴン上にマウスオーバーしたときのハイライト、マウスを外したときに元に戻す、クリックしたらズームする、という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;
}

image.png

色の目安を表示する

  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] ? '&ndash;' + 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;
}

image.png

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

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 を調査・使用することになりドキュメントを舐めまわしてましたが、ドキュメントに記載されていないことが多々あったので嫌いです。
デザインの規約も多すぎです。少しは妥協できんのか…

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

『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/

参考

  1. 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の$変数をメインで使いそうな気がしますので
とりあえずはこんな感じなのでした。

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

E2Eテスト 番外編 -シャープで画像連結-

前回の記事では、reg-cliを使った画像差分比較について紹介しました。
E2Eテストの始め方 番外編 - reg-cliで差分比較 -
そして今回は、TestCafeで撮影した複数枚のスクリーンショット画像を連結するためのsharpについて書いていきたいと思います。

sharp とは

某電機メーカーが強すぎてググラビリティが残念なライブラリ。。。:weary:
同じようなライブラリとしてImageMagickやGraphicsMagickも有名ですが、ドキュメントによるとそれらより4倍~5倍速いそうです。
読み込みはJPEG,PNG,WebP,TIFF,GIF,SVGをサポート。
様々なサイズのJPEG,PNG,WebPに変換可能で、リサイズ以外に回転、抽出、合成、ガンマ補正、圧縮などの操作も可能となっています。
今回sharpを選んだ理由としてImageMagickらはしばらくメンテナンスされていない(sharpは執筆時点で10h前と活発に動いてる)ことと、処理の速さでsharpを採用しました。

シャープならサイズも画質も自由に選べる!
sharp.png

install

$ npm install sharp

usage

convert.js
const sharp = require('sharp');

【ソース全文】

convert.js
const 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に呼び出した画像パスの配列imgPcimgSpを指定し、
第二引数のimageNameには出力される画像のファイル名を指定します。
(デフォルトのピクセル制限(268402689)を超えてしまうので複数枚に分けて出力しています)

convert.js
const 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.js
const 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

完成!

横並びにがっちゃんこして出力してくれました!
各ページフルサイズのスクリーンショットを連結させると出力サイズがかなり大きくなるので圧縮率を大きくしたり、連結枚数を少なくして何枚かに分けるなど工夫が必要ですが良い感じにできたのではないかと思います:ok_hand:
sample.png

参考

NodeJSの画像処理ライブラリ「sharp」を使って画像を連結する

公式

sharp
GitHub

SHARP Be Original

SHARP

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

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 の順番が変わります。

修正前

image.png

修正後

image.png

しかし、このままでは不要な(利用していない)import が残っています。
これも自動で消したいです。

その設定を入れていきます。

# .eslint.yaml
...
plugins:
...
  - unused-imports
rules: 
...
  "@typescript-eslint/no-unused-vars": off
  unused-imports/no-unused-imports-ts: warn
...
修正後

Screen Shot 2020-10-23 at 14.45.21.png

不要な import が消えました。

おわりに

複数人で開発する時に、コードフォーマッタや elinter で記述ルールを揃えると開発速度が一気に加速します。

今回紹介した方法よりも良い方法や、他にも記述改善するためのテクニックがあればぜひ教えてください。

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

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

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

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.loaderrules.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.js
  plugins: [
-   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.moduleIdsoptimization.chunkIdsの設定を削除することを検討してください。
デフォルトだとproductionモードでは長めのキャッシュを、developmentモードではデバックをサポートしますよ。

?

ということで、色々検討した結果moduleIdsも外すことにしています...

webpack.config.js
  optimization: {
-    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.js
  module: {
    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.  ビルドワーニング。 ワーニングメッセージは改善点を示してくれます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アコーディオンパネル使い方

アコーディオンパネル

スクリーンショット 2020-10-23 9.09.30.png

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);
      });
    });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

引数を使って抽象度のあるメソッドを作る

下記の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); // ボンバ...!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の技術を開発する上での作業場所という意味になります。
サーバーで実行されるサーバーサイドの技術は、もちろんそのサーバーを準備する必要が
あり、設定や手順が厄介なものがありますが、フロントエンドの実行する場所はブラウザです。
ブラウザと文字が書けるエディタがあればプラクティスが可能です。

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

誤った情報をpushしてしまった時の対処法

この記事で分かること

  • 誤った情報をpushしてしまった
  • 誤った情報をcommitしてしまった

誤った情報をpushしてしまった

pushした情報をローカルリポジトリに戻すことはできません。したがって、pushする前に作業をしているブランチは正しいのかどうか、必ず確認するようにしましょう。
それでも間違ってpushしてしまうことがあります。ローカルリポジトリにその情報は戻せないものの、リモートリポジトリにある誤ったcommit情報は取り消すことができます

commitを取り消す方法

cbe00f28b88d5f18b9c71024034afe3d.png

commitを取り消すためには、revertと呼ばれる技術を用います
間違ってpushしたcommitを取り消すことができます。commitを削除するのではなく、「指定するcommitを取り消すためのcommit」を追加で行います。
revertはcommitされた変更と逆になる変更を追加することで、commitを取り消します

誤った情報をcommitしてしまった

83c821f4a185cf767e80dcde53e109da.png

現場からは以上です!

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

球上で互いに距離をおいてみた

See the Pen MovingPointsToKeepDistanceOnTheSphereUsingThreeJs by kob58im (@kob58im) on CodePen.

やっていること:球上で、互いの点間の距離の最小値が増加する側に点を徐々に移動させています。

やりたかったこと:正多面体とかの頂点に落ち着かないかなー・・・と思ったけど局所最適解ちっくなとこに落ち着いてしまうので無理でした。

コロナ禍っぽいタイトルになったのは偶然です。。

参考サイト

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