Skip to content

Latest commit

 

History

History
328 lines (226 loc) · 8.96 KB

210-文章详情.md

File metadata and controls

328 lines (226 loc) · 8.96 KB

上一章做好了文章列表,紧接着就是实现文章详情页面了。

从列表到详情,首当其冲的问题就是页面如何跳转。

传统模式的跳转是由 Django 后端分配路由。不过本教程既然采用了前后端分离的模式,那就打算抛弃后端路由,采用前端路由的方式来实现页面跳转。

准备工作

首先安装 Vue 的官方前端路由库 vue-router

> npm install vue-router@4

...

+ vue-router@4.0.2
added 1 package in 9.143s

笔者这里安装到的 4.0.2 版本。

因为 vue-router 会用到文章的 id 作为动态地址,所以对 Django 后端做一点小更改:

# article/serializers.py

class ArticleBaseSerializer(serializers.HyperlinkedModelSerializer):
    id = serializers.IntegerField(read_only=True)
    
    ...

简单的把文章的 id 值增加到接口数据中。

Router

接下来就正式开始配置前端路由了。

首先把 vue-router 加载到 Vue 实例中:

// frontend/src/main.js

...

import router from './router'

createApp(App).use(router).mount('#app');

Vue 2 不同的是,挂载路由实例时 Vue 3 采用函数式的写法,变得更加美观了。

由于后续页面会越来越多,为了避免 App.vue 越发臃肿,因此必须优化文件结构。

新建 frontend/src/views/ 目录,用来存放现在及将来所有的页面文件。在此目录新建 Home.vue 文件,把之前的首页代码稍加修改搬运过来:

<!--  frontend/src/views/Home.vue  -->

<template>

    <BlogHeader/>

    <ArticleList/>

    <BlogFooter/>

</template>

<script>
    import BlogHeader from '@/components/BlogHeader.vue'
    import BlogFooter from '@/components/BlogFooter.vue'
    import ArticleList from '@/components/ArticleList.vue'

    export default {
        name: 'Home',
        components: {BlogHeader, BlogFooter, ArticleList}
    }
</script>

新增文章详情页面:

<!--  frontend/src/views/ArticleDetail.vue  -->

<template>

    <BlogHeader/>

    <!--  暂时留空  -->

    <BlogFooter/>

</template>

<script>
    import BlogHeader from '@/components/BlogHeader.vue'
    import BlogFooter from '@/components/BlogFooter.vue'

    export default {
        name: 'ArticleDetail',
        components: {BlogHeader, BlogFooter}
    }
</script>

页面暂时只有个壳子,一会儿来添加实际功能。

修改 App.vue

<!--  frontend/src/App.vue  -->

<template>
    <router-view/>
</template>

<script>
    export default {
        name: 'App'
    }
</script>

<style>
    ...
</style>

App.vue 文件中大部分内容都搬走了,只剩一个新增的 <router-view> 标签,它就是各路径所代表的页面的实际渲染位置。比如你现在在 Home 页面,那么 <router-view> 则渲染的是 Home 中的内容。

一套组合拳,App.vue 看起来干净多了。

这些都搞好了之后,新建 frontend/src/router/index.js 文件用于存放路由相关的文件,写入:

// frontend/src/router/index.js

import {createWebHistory, createRouter} from "vue-router";
import Home from "@/views/Home.vue";
import ArticleDetail from "@/views/ArticleDetail.vue";

const routes = [
    {
        path: "/",
        name: "Home",
        component: Home,
    },
    {
        path: "/article/:id",
        name: "ArticleDetail",
        component: ArticleDetail
    }
];

const router = createRouter({
    history: createWebHistory(),
    routes,
});

export default router;
  • 列表 routes 定义了所有需要挂载到路由中的路径,成员为路径 url路径名路径的 vue 对象。详情页面的动态路由采用冒号 :id 的形式来定义。
  • 接着就用 createRouter() 创建 router。参数里的 history 定义具体的路由形式,createWebHashHistory() 为哈希模式(具体路径在 # 符号后面);createWebHistory() 为 HTML5 模式(路径中没有丑陋的 # 符号),此为推荐模式,但是部署时需要额外的配置

各模式的详细介绍看文档

搞定这些后,修改首页的组件代码:

<!--  frontend/src/components/ArticleList.vue  -->

<template>
    <div v-for="...">
        
        <!--<div class="article-title">-->
        <!--{{ article.title }}-->
        <!--</div>-->

        <router-link
                :to="{ name: 'ArticleDetail', params: { id: article.id }}"
                class="article-title"
        >
            {{ article.title }}
        </router-link>

        ...
    </div>
</template>

...

调用 vue-router 不再需要常规的 <a> 标签了,而是 <router-link>

:to 属性指定了跳转位置,注意看动态参数 id 是如何传递的。

在 Vue 中,属性前面的冒号 : 表示此属性被”绑定“了。”绑定“的对象可以是某个动态的参数(比如这里的 id 值),也可以是 Vue 所管理的 data,也可以是 methods。总之,看到冒号就要明白这个属性后面跟着个变量或者表达式,没有冒号就是普通的字符串。冒号 : 实际上是 v-bind: 的缩写。

有一个小问题是由于 router 内部机制,之前给 class="article-title" 写的 padding 样式会失效。解决方式是将其包裹在一个 div 元素中,在此 div 上重新定义 padding。想了解做法的见 Github 仓库中的源码。

Router 骨架就搭建完毕了。此时点击首页的文章标题链接后,应该就顺利跳转到一个只有页眉页脚的详情页面了。

注意查看浏览器控制栏,有任何报错都表明代码不正确。

编写详情页面

接下来就正式写详情页面了。

代码量稍稍有点多,一并贴出来:

<!--  frontend/src/views/ArticleDetail.vue  -->

<template>

    <BlogHeader/>

    <div v-if="article !== null" class="grid-container">
        <div>
            <h1 id="title">{{ article.title }}</h1>
            <p id="subtitle">
                本文由 {{ article.author.username }} 发布于 {{ formatted_time(article.created) }}
            </p>
            <div v-html="article.body_html" class="article-body"></div>
        </div>
        <div>
            <h3>目录</h3>
            <div v-html="article.toc_html" class="toc"></div>
        </div>
    </div>

    <BlogFooter/>

</template>

<script>
    import BlogHeader from '@/components/BlogHeader.vue'
    import BlogFooter from '@/components/BlogFooter.vue'

    import axios from 'axios';


    export default {
        name: 'ArticleDetail',
        components: {BlogHeader, BlogFooter},
        data: function () {
            return {
                article: null
            }
        },
        mounted() {
            axios
                .get('/api/article/' + this.$route.params.id)
                .then(response => (this.article = response.data))
        },
        methods: {
            formatted_time: function (iso_date_string) {
                const date = new Date(iso_date_string);
                return date.toLocaleDateString()
            }
        }
    }
</script>

<style scoped>
    .grid-container {
        display: grid;
        grid-template-columns: 3fr 1fr;
    }


    #title {
        text-align: center;
        font-size: x-large;
    }

    #subtitle {
        text-align: center;
        color: gray;
        font-size: small;
    }

</style>

<style>
    .article-body p img {
        max-width: 100%;
        border-radius: 50px;
        box-shadow: gray 0 0 20px;
    }

    .toc ul {
        list-style-type: none;
    }

    .toc a {
        color: gray;
    }
</style>

先看模板部分:

  • 在渲染文章前,逻辑控制语句 v-if 先确认数据是否存在,避免出现潜在的调用数据不存在的 bug。
  • 由于 body_htmltoc_html 都是后端渲染好的 markdown 文本,需要将其直接转换为 HTML ,所以需要用 v-html 标注。

再看脚本:

  • 通过 $route.params.id 可以获得路由中的动态参数,以此拼接为接口向后端请求数据。

最后看样式

  • .grid-container 简单的给文章内容、目录划分了网格区域。
  • <style> 标签可以有多个,满足“分块强迫症患者”的需求。这里分两个的原因是文章内容、目录都是从原始 HTML 渲染的,不在 scoped 的管理范围内。

大致就这些新知识点了,理解起来似乎也不困难。

最后看看实际效果吧:

用很少的、漂亮的代码完成了看起来还不错的详情页,并且摆脱了手动操作 DOM 的繁琐。

感受如何呢。

Django 的 Markdown 渲染只负责把文章转换为 HTML 文本。如果你仍然觉得其排版简陋,那就需要自己定义对应的 CSS 样式,调整为自己喜欢的外观。不过这就不在本文的讨论范畴内了,进一步了解可参考笔者另一篇文章