Django-Vue搭建个人博客:文章详情
11013 views, 2023/10/16 updated Go to Comments
上一章做好了文章列表,紧接着就是实现文章详情页面了。
从列表到详情,首当其冲的问题就是页面如何跳转。
传统模式的跳转是由 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_html
、toc_html
都是后端渲染好的 markdown 文本,需要将其直接转换为 HTML ,所以需要用v-html
标注。
再看脚本:
- 通过
$route.params.id
可以获得路由中的动态参数,以此拼接为接口向后端请求数据。
最后看样式:
.grid-container
简单的给文章内容、目录划分了网格区域。<style>
标签可以有多个,满足“分块强迫症患者”的需求。这里分两个的原因是文章内容、目录都是从原始 HTML 渲染的,不在scoped
的管理范围内。
大致就这些新知识点了,理解起来似乎也不困难。
最后看看实际效果吧:
用很少的、漂亮的代码完成了看起来还不错的详情页,并且摆脱了手动操作 DOM 的繁琐。
感受如何呢。
Django 的 Markdown 渲染只负责把文章转换为 HTML 文本。如果你仍然觉得其排版简陋,那就需要自己定义对应的 CSS 样式,调整为自己喜欢的外观。不过这就不在本文的讨论范畴内了,进一步了解可参考笔者另一篇文章。