3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

NuxtとGraphQLとAWS(Amplify, appsync, dynamodb, cognito, s3....)を用いてWebアプリチュートリアルPart2

Last updated at Posted at 2021-02-17

概要

やること

  • ブログ一覧表示
  • 新規登録

フロント実装編

ブログ一覧表示

まずはデータの取得なしでデザインのみでコードを書いていきます。

page/index.vue
<template>
  <display />
</template>

<script>
import Display from '../components/readBlog/index'

export default {
  components: {
    Display,
  },
}
</script>

コンポーネントを記述します。

components/readBlog/index.vue
<template>
  <v-card class="mx-auto" max-width="344">
    <v-img
      src="https://cdn.vuetifyjs.com/images/cards/sunshine.jpg"
      height="200px"
    ></v-img>

    <v-card-title> Top western road trips </v-card-title>

    <v-card-subtitle> 1,000 miles of wonder </v-card-subtitle>

    <v-card-actions>
      <v-btn color="orange lighten-2" text> Explore </v-btn>
      <v-spacer></v-spacer>
    </v-card-actions>
  </v-card>
</template>

コンポーネントのコードはvuetifyからのコピペです。

スクリーンショット 2021-02-14 11.54.08.png

このように表示されます。

続いて、データの取得をしたいところですが、dynamoDBにデータがないので、新規作成機能を作り、そのあと取得することにします。

新規作成機能

最初は、デザインからいきます。

pages/write/create.vue
<template>
  <CreateBlog />
</template>

<script>
import CreateBlog from '../../components/writeBlog/create'

export default {
  components: {
    CreateBlog,
  },
}
</script>

コンポーネントを適当に作成します

components/writeBlog/create.vue
<template>
  <v-container>
    <v-row>
      <v-col col="12">
        <input v-model="title" type="text" />
      </v-col>
    </v-row>
    <v-row>
      <v-col col="12">ファイル用</v-col>
    </v-row>
    <v-row>
      <v-col col="12"></v-col>
    </v-row>
  </v-container>
</template>

今、ブログのタイトル、画像の投稿を入れました。
続いて、ブログの内容を書くためにマークダウンに対応したエディタをインストールします。

候補

お好きなのを使ってください。
今回私が使用するプラグインはmavonEditorです。

mavon-editorのインストール

npm install mavon-editor --save
plugins/vue-mavon-editor.js
import Vue from 'vue'
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'

Vue.use(mavonEditor)

次にnuxt.config.jsファイルにプラグインパスを書きます。

nuxt.config.js
  plugins: [
  ...
    { src: '@/plugins/vue-mavon-editor', ssr: false }
//  { src: './plugins/vue-mavon-editor', ssr: false } ドキュメント通りやると私のほうはエラー出たのでこちらを使いました。 
  ],

続いて、mavonEditorの表示です。

components/writeBlog/create.vue
<template>
  <v-container>
    <v-row>
      <v-col col="12">
        <input v-model="title" type="text" />
      </v-col>
    </v-row>
    <v-row>
      <v-col col="12">ファイル用</v-col>
    </v-row>
    <v-row>
<!-- 追加 -->
      <v-col col="12">
        <div class="mavonEditor">
          <no-ssr>
            <mavon-editor
              v-model="handbook"
              :toolbars="markdownOption"
              language="en"   // デフォルトは中国語になるので英語に変更
            />
          </no-ssr>
        </div>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
export default {
  data() {
    return {
      markdownOption: {
        bold: true, // 粗体
        italic: true, // 斜体
        header: true, // hello
        underline: true, // 下划线
        strikethrough: true, // 中划线
        mark: true, // 标记
        superscript: true, // 上角标
        subscript: true, // 下角标
        quote: true, // 引用
        ol: true, // 有序列表
        ul: true, // 无序列表
        link: true, // hi
        // imagelink: true, // 图片链接
        code: true, // code
        table: true, // 表格
        fullscreen: true, // 全屏编辑
        readmodel: true, // 沉浸式阅读
        htmlcode: true, // 展示html源码
        help: true, // 帮助
        /* 1.3.5 */
        undo: true, // 上一步
        redo: true, // 下一步
        trash: true, // 清空
        // save: true, // 保存(触发events中的save事件)
        /* 1.4.2 */
        navigation: true, // 导航目录
        /* 2.1.8 */
        alignleft: true, // 左对齐
        aligncenter: true, // 居中
        alignright: true, // 右对齐
        /* 2.2.1 */
        subfield: true, // 单双栏模式
        preview: true, // 预览
      },
      handbook: '#### how to use mavonEditor in nuxt.js',
    }
  },
  methods: {
    // comfirmImage() {},
  },
}
</script>

// 追加↓
<style scoped>
.mavonEditor {
  width: 100%;
  height: 100%;
}
</style>

追加完了です。
こちらにアクセスしてみると画像のように表示されていればOKです!!
http://localhost:3000/write/create
スクリーンショット 2021-02-16 14.54.07.png

一度レイアウトを整えます。。
タイトル名を表示する場所が背景と同化しているので、、。

components/writeBlog/create.vue
// 略

        <p>Title</p>
        <input class="text" v-model="title" type="text" />
      </v-col>
    </v-row>
    <v-row>
      <!--      <v-col col="12"><input type="file" @change="confirmImage" /></v-col>-->
    </v-row>
    <v-row>
      <v-col col="12">
        <p>Contents</p>
        <div class="mavonEditor">
          <no-ssr>
            <mavon-editor
              v-model="handbook"
              :toolbars="markdownOption"
              language="en"
            />
          </no-ssr>

// 略
<style scoped>
// 略
.text {
  color: white;
  padding: 0px 10px;
  font-size: 30px;
  border: 1px solid #424242;
  border-radius: 20px;
  height: 40px;
  width: 100%;
}

以下のように書き換えましょう。。

これでスタイル調整完了です。
以下のように表示されます。
スクリーンショット 2021-02-16 15.11.44.png

次に投稿実装と画像のアップロードも一緒にやります。

まずは画像のアップロード機能を実装しましょう。(s3以外)

components/writeBlog/create.vue
// 略
    <v-row>
      <v-col col="12">
        <p>Title</p>
        <input class="text" v-model="title" type="text" />
      </v-col>
    </v-row>
// 画像追加用
    <v-row>
      <v-col col="6">
        <input class="image" type="file" @change="confirmImage" />
      </v-col>
      <v-col col="6">
        <span v-if="confirmedImage">
          <img class="c-image" :src="confirmedImage" alt />
        </span>
      </v-col>
    </v-row>
    <v-row>
      <v-col col="12">
        <p>Contents</p>
// 略

<script>
export default {
  data() {
    return {
      markdownOption: {
        // 略
      },
      handbook: '#### how to use mavonEditor in nuxt.js',
//追加
      file: '',
      confirmedImage: '',
      message: '',
    }
  },
  methods: {
// 追加
    confirmImage(event) {
      this.message = ''
      this.file = event.target.files[0]
      if (!this.file.type.match('image.*')) {
        this.message = '画像ファイルを選択して下さい'
        this.confirmedImage = ''
        return
      }
      this.createImage(this.file)
    },
    createImage(file) {
      const reader = new FileReader()
      reader.readAsDataURL(file)
      reader.onload = (e) => {
        this.confirmedImage = e.target.result
      }
    },
  },
}
</script>

<style scoped>
// 追加
.image {
  height: 150px;
}
.c-image {
  width: 300px;
  height: 150px;
}
</style>

これで画像アップロード用の機能実装完了です。残りはs3とgraphqlの実装です。

s3の導入

ターミナルで以下のコードを実行

% amplify add storage
? Please select from one of the below mentioned services: Content (Images, audio, video, etc.)
? Please provide a friendly name for your resource that will be used to label this category in the project: blog
? Please provide bucket name: blog
? Who should have access: Auth and guest users
? What kind of access do you want for Authenticated users? create/update, read, delete
? What kind of access do you want for Guest users? read
? Do you want to add a Lambda Trigger for your S3 Bucket? No

以下のコマンドを打ってバックエンドに反映させましょう✌️

% amplify push

これで導入完了です。
本当かよ!ってくらい早くないっすか??
気になる人は簡単にチェックする方法があって、s3のページに行くのもいいですが、
aws-export.jsというファイルを見るとs3の項目が表示されているはずです。

% amplify status

↑こちらでも確認できます。

投稿機能仕上げ

投稿機能ラストです!!
投稿しましょう。

components/writeBlog/create.vue
<template>
<!-- 略 -->
      <v-col col="12">
        <p>Contents</p>
        <div class="mavonEditor">
          <no-ssr>
            <mavon-editor
              v-model="handbook"
              :toolbars="markdownOption"
              language="en"
            />
          </no-ssr>
        </div>
      </v-col>
    </v-row>
    <v-row>
      <v-col class="btn-box" col="12">
        <input class="btn" type="button" @click="submit" value="投稿" />
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
// 追加
import { API, Storage } from 'aws-amplify'
import awsmobile from '../../aws-exports'
import { createBlog } from '../../graphql/mutations'

export default {
  data() {
    return {
// 略
      handbook: '#### how to use mavonEditor in nuxt.js',
      imageData: '',
      confirmedImage: '',
      imageUrl: '',
      message: '',
    }
  },
  methods: {
// 略
    async submit() {
      const filePath = `cuisine/${this.imageData.name}`
      const imageUrlDefault =
        'https://' +
        awsmobile.aws_user_files_s3_bucket +
        '.s3-' +
        awsmobile.aws_user_files_s3_bucket_region +
        '.amazonaws.com/public/'
      await Storage.put(filePath, this.imageData).then((result) => {
        this.imageUrl = imageUrlDefault + filePath
      })
      const createBlogInfo = {
        title: this.title,
        image_url: this.imageUrl,
        content: this.handbook,
      }
      await API.graphql({
        query: createBlog,
        variables: { input: createBlogInfo },
      })
      window.location.href = '/'
    },
  },
}
</script>

<style scoped>
/* 略 */
.btn-box {
  text-align: center;
}
.btn {
  border: 1px solid #000000;
  background-color: white;
  color: #424242;
  width: 90px;
  height: 30px;
  border-radius: 10px;
}
</style>

さ!これで投稿ボタンを押して投稿!!。
スクリーンショット 2021-02-16 17.19.26.png

あれ、、、。エラー??

違います。
実はs3バケットを作成した時の権限の話で、s3に書き込みができるのは、認証済みユーザのみ!としたはずです、、。
つまり、ログイン必須

pages/write/create.vue
<template>
  <amplify-authenticator>
    <CreateBlog />
    <amplify-sign-out></amplify-sign-out>
  </amplify-authenticator>
</template>

<script>
import CreateBlog from '../../components/writeBlog/create'

export default {
  name: 'Create',
  components: {
    CreateBlog,
  },
}
</script>

amplifyのauthタグの追加を行いました。
プラスで、name属性の追加を行いました。どうやらこれが無いとダメっぽいので、、、。

さ、完了です。投稿してみましょう。

投稿が完了すると,トップページへ画面遷移しますが、トップページでは、データの取得を行っていません。なのでデータの取得を行います。

page/index.vue
<template>
  <div>
    <Display v-for="post in posts" :key="post.id" :post="post" />
  </div>
</template>

<script>
import { API } from 'aws-amplify'
import Display from '../components/readBlog/index'
import { listBlogs } from '../graphql/queries'

export default {
  components: {
    Display,
  },
  data() {
    return {
      posts: [],
    }
  },
  created() {
    this.getBlog()
  },
  methods: {
    async getBlog() {
      const posts = await API.graphql({
        query: listBlogs,
      })
      this.posts = posts.data.listBlogs.items
    },
  },
}
</script>

次にコンポーネント

components/readBlog/index.vue
<template>
  <v-card class="mx-auto" max-width="344">
    <v-img :src="post.image_url" height="200px"></v-img>

    <v-card-title>{{ post.title }}</v-card-title>

    <v-card-subtitle>{{ post.createdAt }}</v-card-subtitle>

    <v-card-actions>
      <v-btn color="orange lighten-2" text>
        Explore
      </v-btn>
      <v-spacer></v-spacer>
    </v-card-actions>
  </v-card>
</template>

<script>
export default {
  name: 'PostCard',
  props: ['post'],
}
</script>

スクリーンショット 2021-02-17 13.07.18.png

このようにデータ取得できていたら成功です。
デザインが少しおかしいですが、ま、いいでしょう。

次に詳細画面への画面遷移です。

まずは、一覧表示の画面で、詳細へ飛べるようにリンクを貼りましょう

components/readBlog/index.vue
<template>
  <v-card class="mx-auto" max-width="344">
    <v-img :src="post.image_url" height="200px"></v-img>

    <v-card-title>{{ post.title }}</v-card-title>

    <v-card-subtitle>{{ post.createdAt }}</v-card-subtitle>

    <v-card-actions>
      <v-btn color="orange lighten-2" text :to="'/detail/' + post.id">
        Explore
      </v-btn>
      <v-spacer></v-spacer>
    </v-card-actions>
  </v-card>
</template>

<script>
export default {
  name: 'PostCard',
  props: ['post'],
}
</script>

あとは、詳細ページ用のファイルを作成して完了です。

pages/detail/_id.vue
<template>
  <DetailCard :key="blog.id" :blog="blog" />
</template>

<script>
import { API } from 'aws-amplify'
import { getBlog } from '../../graphql/queries'
import DetailCard from '../../components/readBlog/detail'

export default {
  components: {
    DetailCard,
  },
  data() {
    return {
      blog: [],
    }
  },
  created() {
    this.getCuisineDetail()
  },
  methods: {
    async getCuisineDetail() {
      // eslint-disable-next-line no-undef
      const detailBlog = await API.graphql({
        query: getBlog,
        variables: {
          id: this.$route.params.id,
        },
      })
      this.blog = detailBlog.data.getBlog
    },
  },
}
</script>

次はコンポーネント

components/readBlog/detail.vue
<template>
  <v-container>
    <v-row>
      <v-col col="12">
        <img class="image" :src="blog.image_url" alt="test" />
      </v-col>
    </v-row>
    <v-row>
      <v-col col="12">
        <h2>{{ blog.title }}</h2>
      </v-col>
    </v-row>
    <v-row>
      <v-col col="12">
        <div class="mavonEditor">
          <no-ssr>
            <mavon-editor
              v-model="content"
              language="ja"
              :subfield="false"
              defaultOpen="preview"
              :toolbars="false"
            />
          </no-ssr>
        </div>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
export default {
  name: 'DetailCard',
  props: ['blog'],
  data() {
    return {
      content: '',
    }
  },
  created() {
    this.data()
  },
  methods: {
    data() {
      this.content = this.blog.content
    },
  },
}
</script>

<style scoped>
.image {
  width: 100%;
  height: 300px;
}
</style>

ブログ一覧と詳細

01.gif

以上でパート2終わりです。

ソースコード

part2のソースコードです。

続きのURL

参考

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?