Django-Vue搭建个人博客:文章详情
9698 views, 2021/03/21 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 样式,调整为自己喜欢的外观。不过这就不在本文的讨论范畴内了,进一步了解可参考笔者另一篇文章。