シンプルブログ、URLの書き換え・直接アクセスに対応

Vue.js

概要

DRFとVue.jsで、シンプルブログを作るシリーズの一つです。

現状では、ページ移動や検索後にURLが書き換わりません。例えば、カテゴリがAの一覧ページのURLをコピーしたい場合はどうでしょうか。サイトを訪れた人がURLをコピーしてメールしたり、自分のサイトのリンクに追加したいかもしれません。

PostListコンポーネント

まず、次ページ・前ページを押した際にURLも書き換わるようにします。PostList.vueのmethodsを少し書き換えます。

        methods: {
            ...mapActions([UPDATE_POSTS]),
            getPostPrevious() {
                const url = new URL(this.getPreviousURL)
                const keyword = url.searchParams.get('keyword') || ''
                const category = url.searchParams.get('category') || ''
                const page = url.searchParams.get('page') || 1
                this.$router.push({name: 'posts', query: {keyword, category, page}})
                this.$http(this.getPreviousURL)
                    .then(response => {
                        return response.json()
                    })
                    .then(data => {
                        this[UPDATE_POSTS](data)
                    })
            },
            getPostNext() {
                const url = new URL(this.getNextURL)
                const keyword = url.searchParams.get('keyword') || ''
                const category = url.searchParams.get('category') || ''
                const page = url.searchParams.get('page')
                this.$router.push({name: 'posts', query: {keyword, category, page}})
                this.$http(this.getNextURL)
                    .then(response => {
                        return response.json()
                    })
                    .then(data => {
                        this[UPDATE_POSTS](data)
                    })
            }
        },

前ページ・次ページへのAPIのURL...http://127.0.0.1:8000/design-note/api/posts/?category=1&keyword=hello&page=2といったものですが、これはDjango側から返されています。this.getPreviousURL等ですね。ここのGETパラメータ部分を取り出して(?keyword=hello&category=1&page=2)、router.pushのqueryに渡しています。こうすると、GETパラメータ付きのURLを作ってくれるのです。結果的に、http://127.0.0.1:8080/?keyword=hello&category=1&page=2といったURLに書き換わります。

このURLを直接アクセスした場合でも動作するようにしましょう。createdを変更します。

        created() {
                let postURL = this.$httpPosts
                const params = this.$route.query
                const queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&')
                if (queryString) {
                    postURL += '?' + queryString
                }
            this.$http(postURL)
                .then(response => {
                    return response.json()
                })
                .then(data => {
                    this[UPDATE_POSTS](data)
                })
        }

URL直接アクセス時なら、this.$route.queryのようにしてGETパラメータを取得できます。そのGETパラメータをcategory=1&keyword=hello&page=2みたいな文字列にして、APIのURLに追加し、リクエストを送っています。

良い感じになってきましたが、一覧ページにて「次ページへ移動→ブラウザバック」のような操作には対応していません。Vueではページに変化がなさそうなとき、今使っているコンポーネントをそのまま使うのです。詳細ページから一覧ページへのブラウザバック等は問題ないのですが、一覧ページから一覧ページへのブラウザバック(GETパラメータだけ違う)はコンポーネントがそのまま使われます。

こういったときは、watchでルートパラメータの変化を捕まえることができます。中身の処理は、先ほどと同じ。

        watch: {
            '$route'() {
                let postURL = this.$httpPosts
                const params = this.$route.query
                const queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&')
                if (queryString) {
                    postURL += '?' + queryString
                }
                this.$http(postURL)
                    .then(response => {
                        return response.json()
                    })
                    .then(data => {
                        this[UPDATE_POSTS](data)
                    })
            }
        },

これでもまだ問題があって、ルートパラメータの変化なので、単純に次ページへ移動したときもこのウォッチャーが呼ばれます。次ページ移動時は、getPostNextメソッド内でAjax通信をしていました。ウォッチャーでもAjaxでのリクエストを行うので、二度手間です。getPostNextメソッド内のAjax通信部分をなくすか、getPostNextではルートパラメータの変化だけをさせるかになります。今回は後者の方法にします。

それらを踏まえて、最終的には次のようになりました。

<template>
    <main class="container">
        <p id="lead">{{postCount}}件中 {{postRangeFirst}}~{{postRangeLast}}件を一覧表示</p>
        <section>
            <router-link :to="{name: 'detail', params: {id: post.id}}" v-for="post of postList" :key="post.id"
                         class="post">
                <article>
                    <figure>
                        <img :src="post.thumbnail" :alt="post.title" class="thumbnail">
                    </figure>
                    <p class="post-category" :style="{'color': post.category.color}">{{post.category.name}}</p>
                    <h2 class="post-title">{{post.title}}</h2>
                    <p class="post-lead">{{post.lead_text}}</p>
                </article>
            </router-link>
        </section>
        <hr class="divider">
        <nav id="page">
            <router-link v-if="hasPrevious" :to="getPostPreviousURL" id="back"><img src="@/assets/back.png">
            </router-link>
            <span>Page {{postCurrentPageNumber}}</span>
            <router-link v-if="hasNext" :to="getPostNextURL" id="next"><img src="@/assets/next.png"></router-link>
        </nav>
    </main>
</template>

<script>
    import {mapGetters, mapActions} from 'vuex'
    import {UPDATE_POSTS} from "@/store/mutation-types";

    export default {
        name: 'post-list',
        watch: {
            '$route'() {
                this.getPosts()
            }
        },
        created() {
            this.getPosts()
        },
        computed: {
            ...mapGetters([
                'postList', 'postCount', 'postRangeFirst', 'postRangeLast',
                'postCurrentPageNumber', 'hasPrevious', 'hasNext', 'getPreviousURL', 'getNextURL'
            ]),
            getPostPreviousURL() {
                const url = new URL(this.getPreviousURL)
                const keyword = url.searchParams.get('keyword') || ''
                const category = url.searchParams.get('category') || ''
                const page = url.searchParams.get('page') || 1
                return this.$router.resolve({
                    name: 'posts',
                    query: {keyword, category, page}
                }).route.fullPath
            },
            getPostNextURL() {
                const url = new URL(this.getNextURL)
                const keyword = url.searchParams.get('keyword') || ''
                const category = url.searchParams.get('category') || ''
                const page = url.searchParams.get('page')
                return this.$router.resolve({
                    name: 'posts',
                    query: {keyword, category, page}
                }).route.fullPath
            }
        },
        methods: {
            ...mapActions([UPDATE_POSTS]),
            getPosts() {
                let postURL = this.$httpPosts
                const params = this.$route.query
                const queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&')
                if (queryString) {
                    postURL += '?' + queryString
                }
                this.$http(postURL)
                    .then(response => {
                        return response.json()
                    })
                    .then(data => {
                        this[UPDATE_POSTS](data)
                    })
            },
        }
    }
</script>

コンポーネントが作成されたとき(created)や、ルートパラメータが変化(watch $route)したときはgetPostsメソッドを呼びます。中身は今までどおり、GETパラメータがあればそれをAPIのURLに追加して記事一覧を取得しています。

前ページや次ページの処理は、router-linkを使って単純なページ移動にしました。先ほど書いたとおり、ウォッチでルートパラメータの変化を監視しているので、単純なページ移動でも充分になったのです。遷移先URLの取得はcomputed内に定義しています。今までの処理とほぼ同様ですが、<router-link>を使っているのでrouter.pushとかは不要になり、URLだけを返せばいいので、$router.resolve().route.fullPathのように書いています。

検索時にもURLが書き換わるようにしましょう。Header.vuesearchメソッドを修正します。

            search() {
                this.$router.push({name: 'posts', query: {page: 1, keyword: this.keyword, category: this.selected}})
            },

ここもrouter.pushだけで良くなりました。ルートパラメータの変化はPostList.vue側で検知され、書き換わったURLをもとにデータの一覧を取得してくれます。

URLアクセス時に、キーワードやカテゴリの指定がURL内にあれば、それをもとに選択済みにするようにも変更しましょう。

        data() {
            return {
                keyword: this.$route.query.keyword || '',
                selected: this.$route.query.category || '',
            }
        },
        watch: {
            '$route'() {
                this.keyword = this.$route.query.keyword || ''
                this.selected = this.$route.query.category || ''
            }
        },

this.$route.query.keywordのようにして、URL内のGETパラメータを探し、dataオプションのプロパティに設定していきます。なかった場合は空文字列です。ここでのwatch $routeはどういう動きをするかというと、サイトタイトルをクリックで真っ新なトップページへ戻るようになっています(<router-link :to="{name: 'posts'}">Design Note</router-link>)が、その場合にも検索欄がリセットされるようにするためです。

前へ戻る時にスクロール位置も再現

次のページへ移動してブラウザバックをしたとき、前のページの一覧が表示されるようになっています。これにプラスして、ブラウザバックの際にスクロール位置も再現するようにしましょう。

これは簡単で、router/index.jsを次のようにするだけです。

const router = new VueRouter({
    mode: 'history',
    base: process.env.BASE_URL,
    routes,
    scrollBehavior(to, from, savedPosition) {
        if (savedPosition) {
            return savedPosition
        } else {
            return {x: 0, y: 0}
        }
    }
})

scrollBehaviorの部分が追加されました。

柔軟に前へ戻る

記事詳細ページ上の戻るアイコンですが、現状では真っ新なトップページへ移動します。しかし、検索結果だったり幾つかページ移動した場所から、記事をクリックで詳細ページへ入ることも考えられます。この場合、トップページではなく、直前の検索結果等のページに移動する方が親切です。つまりブラウザバックと同様の処理にしたいのです。

ですが、記事詳細ページへ直接アクセスしてくるケースもありえます。この場合はブラウザバックではなく、このサイトのトップに移動するのが良さそうです。そんなわけで、柔軟に前に戻るように実装します。

Post.vueの、まずはtemplate部分を書き換えます。

<nav id="back"><a @click="goBack" title="前ページへ戻る"><img src="@/assets/back.png"></a></nav>

クリックで、goBackメソッドを呼ぶようにしました。script部分は次のようにします。

<script>
        data() {
            return {
                post: null,
                hasBefore: false,  // 追加
            }
        },

        // 追加
        beforeRouteEnter(to, from, next) {
            next(component => {
                if (from.name) {
                    component.hasBefore = true
                }
            })
        },

        methods: {
            // 追加
            goBack() {
                if (this.hasBefore) {
                    this.$router.go(-1)
                } else {
                    this.$router.push({name: 'posts'})
                }
            },
        }
    }
</script>

dataオプションにhasBeforeというオプションを持たせました。これは、サイト内での前ページがあるかどうかのフラグです。goBackメソッドではこの値を調べて、サイト内の履歴があればthis.$router.go(-1)で前のページへ戻り、そうでなければトップページへ移動しています。

beforeRouteEnterは、/detail/id というURLへ移動してきたときに呼ばれるフックです。from.nameがあるときというのは、サイト内での移動ということになりますので、hasBeforeをtrueにします。

Relation Posts

Comment

記事にコメントする

まだコメントはありません。