泽恩小站-教程区 Help

文章详情

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

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

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

准备工作

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

> npm install vue-router@4 ... + vue-router@4.0.2 added 1 package in 9.143s

因为 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');

由于后续页面会越来越多,为了避免 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 中的内容。

这些都搞好了之后,新建 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 骨架就搭建完毕了。此时点击首页的文章标题链接后,应该就顺利跳转到一个只有页眉页脚的详情页面了。

编写详情页面

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

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

<!-- 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 的管理范围内。

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

最后看看实际效果吧:

P210 1

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

感受如何呢。

Last modified: 06 January 2025