博客网站最重要的是有一个给用户浏览文章的页面,也就是博客网站的前台,用户通过这个页面可以查找文章,浏览文章详情,评论,点赞等。
大家好,我是落霞孤鹜
,上一篇我们已经博客的管理后台功能,这一章我们开始搭建博客的前台,实现对博客网站文章的查看,浏览,评论,点赞等功能。我同样按照一个完整的功能,从需求分析到代码编写来阐述如何实现。
一、需求分析
作为一个完整的博客网站,前台是内容呈现的核心部分,大部分的博客搭建文章着重介绍的也是这一部分。这里我们从实际需要出发,整理了如下需求要点:
- 首页: 主要展示整个博客网站的文章,一般按照发布时间倒序呈现,展示标题,摘要,浏览量,点赞量,评论量,留言量等内容,提供标签筛选
- 文章详情:主要用来展示文章的详情,涵盖文章的所有细节,同时提供文章的章节目录导航、点赞、评论功能。
- 文章分类:通过分类呈现文章列表,方便用户通过类型快速查找感兴趣的文章。
- 归档:按照年份倒序呈现博客网站的文章列表。
- 关于:一般介绍博客的博主情况和博客网站的主题信息。
以上功能也算是一套简单的个人博客网站的核心功能框架。
二、后端接口部分
后端接口部分在上一篇的管理后端中,已经全部实现,这里就不再重复介绍。
三、前端界面部分
前端按照需求,我们从首页,文章详情,文章分类,归档,关于五个部分呈现。这一部分的页面全部放在src/views/client
文件中。
首页的功能,实际上是一个文章列表展示,而分类页面是增加了一个分类树的文章列表展示,因此在设计页面的时候,将文章展示列表作为一个组件,这样分类展示可以通过列表和分类两个组件拼装而成。
关于页面可以类比为一篇介绍博客和博主的文章详情页面,因此,可以将文章详情展示作为一个组件,这样可以完成对详情页和关于页的支撑。
3.1 首页
3.1.1 Type
层
在处理管理后台时,已经在src/types/index.ts
文件中定义好了文章相关的interfaceArticle
, ArticleArray
, ArticleParams
。
3.1.2 API
层
在处理管理后台时,已经在src/api/service.ts
文件中定义好了方法getArticleList
。
3.1.3 Component
层
依据上面的分析,我们需要将文章列表封装成一个组件,因此在src/components
下新增文件ArticleList.vue
,通过点击文章,在一个新的页面中查看文章详情,代码如下:
处理页面在请求后端时的加载状态组件和结束加载状态后的组件。
在src/components
下新增文件Loading.vue
,代码如下:
在src/components
下新增文件EndLoading.vue
,代码如下:
--------------------------- 我也是有底线的啦 ---------------------------
3.1.4 Utils层
在文章列表中,我们为了更好的体验,对图片展示提供了限流处理和无极滚动加载。工具方法有:getDocumentHeight
, getQueryStringByName
, getScrollTop
, getWindowHeight
, throttle
,在src/utils/index.ts
增加代码:
// fn是我们需要包装的事件回调, delay是时间间隔的阈值
export function throttle(fn: Function, delay: number) {
let last = 0,
timer: any = null;
return function (this: any) {
let context = (this as any);
let args = arguments;
let now = +new Date();
if (now - last < delay) {
clearTimeout(timer);
timer = setTimeout(function () {
last = now;
fn.apply(context, args);
}, delay);
} else {
last = now;
fn.apply(context, args);
}
};
}
//根据 QueryString 参数名称获取值
export function getQueryStringByName(name: string) {
let result = window.location.search.match(
new RegExp("[?&]" + name + "=([^&]+)", "i")
);
if (result == null || result.length < 1) {
return "";
}
return result[1];
}
//获取页面顶部被卷起来的高度
export function getScrollTop() {
return Math.max(
//chrome
document.body.scrollTop,
//firefox/IE
document.documentElement.scrollTop
);
}
//获取页面文档的总高度
export function getDocumentHeight() {
//现代浏览器(IE9+和其他浏览器)和IE8的document.body.scrollHeight和document.documentElement.scrollHeight都可以
return Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
);
}
//页面浏览器视口的高度
export function getWindowHeight() {
return document.compatMode === "CSS1Compat"
? document.documentElement.clientHeight
: document.body.clientHeight;
}
3.1.5 View
层
修改src/views/client/Home.vue
文件,编写如下代码:
{{ state.tag_name }} 相关的文章:
3.1.5 Router
层
由于主页的路由已经在src/route/index.ts
文件中配置,这里增加Articles的路由。
{
path: "/articles",
name: "Articles",
component: () =>
import("../views/admin/Home.vue")
},
3.1.6 Less
在src
下新增文件夹less
,新增index.less
代码如下:
body {
padding: 10px;
margin: 0;
}
a {
text-decoration: none;
}
.clearfix:before,
.clearfix:after {
display: table;
content: '';
}
.layout {
display: flex;
}
.right {
width: 350px;
}
.left {
flex: 1;
padding-right: 20px !important;
}
.clearfix:after {
clear: both;
}
.fl {
float: left;
}
.fr {
float: right;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 1em;
}
strong {
font-weight: bold;
}
p > code:not([class]) {
padding: 2px 4px;
font-size: 90%;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 4px;
}
img {
max-width: 100%;
}
.container {
width: 1200px;
margin: 0 auto;
}
.article-detail {
img {
/* 图片居中 */
display: flex;
max-width: 100%;
margin: 0 auto;
}
table {
text-align: center;
border: 1px solid #eee;
margin-bottom: 1.5em;
}
th,
td {
// text-align: center;
padding: 0.5em;
}
tr:nth-child(2n) {
background: #f7f7f7;
}
}
.article-detail {
font-size: 16px;
line-height: 30px;
}
.anchor-fix {
position: relative !important;
top: -80px !important;
display: block !important;
height: 0 !important;
overflow: hidden !important;
}
.article-detail .desc ul,
.article-detail .desc ol {
color: #333333;
margin: 1.5em 0 0 25px;
}
.article-detail .desc h1,
.article-detail .desc h2 {
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.article-detail .desc a {
color: #009a61;
}
.article-detail blockquote {
margin: 0 0 1em;
background-color: rgb(220, 230, 240);
padding: 1em 0 0.5em 0.5em;
border-left: 6px solid rgb(181, 204, 226);
}
在src/App.vue
的style
部分,增加导入index.less
的代码
@import url("./less/index.less");
3.2 文章详情
文章详情通过路径中的query
参数传递文章id的方式区别不同的文章,这样的好处是方便文章可以通过url实现分享,比如想发表在公众号中,原文链接就可以直接用该URL
3.2.1 Type
层
文章详情中涉及到文章, 分类,标签,点赞、评论,结合之前已经定义的内容,在src/types/index.ts
文件中增加代码如下:
export interface Like {
article: number,
user: number,
}
3.2.2 API
层
在src/api/service.ts
编写如下代码:
export function postLikeArticle(data: Like) {
return request({
url: '/like/',
method: 'post',
data,
})
}
export function getArticleComments(articleId: number) {
return request({
url: '/comment/',
method: 'get',
params: {
article: articleId,
},
}) as unknown as ResponseData
}
export function addComment(data: CommentPara) {
return request({
url: '/comment/',
method: 'post',
data
})
}
3.2.3 Component
层
这里需要用到评论列表,评论组件。
在src/components
下新增文件Comment.vue
,负责新增评论,代码如下:
发 送
在src/components
下新增文件CommentList.vue
,负责展示文章的评论列表,代码如下:
{{ numbers }} 条评论
{{ item.user_info.name
}}{{ item.user_info.role === "Admin" ? "(作者)" : "" }}
{{ formatTime(item.created_at) }}
{{ item.content }}
{{ e.user_info.name }}
{{ e.user_info.role === "Admin" ? "(作者)" : "" }}
{{ formatTime(e.created_at) }}
{{ e.content }}
3.2.4 Util层
由于我们编写的文章正文是用markdown的方式记录的,而在博客网站上需要展示的是HTML,因此我们需要在展示之前将markdown转换成html,同时能展示文章的章节目录,所以先安装依赖
yarn add [email protected]
在src/utils
下新增文件markdown.ts
,编写代码如下:
import highlight from 'highlight.js'
// @ts-ignore
import marked from 'marked'
const tocObj = {
add: function (text: any, level: any) {
let anchor = `#toc${level}${++this.index}`;
this.toc.push({anchor: anchor, level: level, text: text});
return anchor;
},
toHTML: function () {
let levelStack: any = [];
let result = "";
const addStartUL = () => {
result += '';
};
const addEndUL = () => {
result += "
\n";
};
const addLI = (anchor: any, text: any) => {
result +=
'' + text + " \n";
};
this.toc.forEach(function (item: any) {
let levelIndex = levelStack.indexOf(item.level);
// 没有找到相应level的ul标签,则将li放入新增的ul中
if (levelIndex === -1) {
levelStack.unshift(item.level);
addStartUL();
addLI(item.anchor, item.text);
} // 找到了相应level的ul标签,并且在栈顶的位置则直接将li放在此ul下
else if (levelIndex === 0) {
addLI(item.anchor, item.text);
} // 找到了相应level的ul标签,但是不在栈顶位置,需要将之前的所有level出栈并且打上闭合标签,最后新增li
else {
while (levelIndex--) {
levelStack.shift();
addEndUL();
}
addLI(item.anchor, item.text);
}
});
// 如果栈中还有level,全部出栈打上闭合标签
while (levelStack.length) {
levelStack.shift();
addEndUL();
}
// 清理先前数据供下次使用
this.toc = [];
this.index = 0;
return result;
},
toc: [] as any,
index: 0
};
class MarkUtils {
private readonly rendererMD: any;
constructor() {
this.rendererMD = new marked.Renderer() as any;
this.rendererMD.heading = function (text: any, level: any, raw: any) {
let anchor = tocObj.add(text, level);
return `${text} \n`;
};
this.rendererMD.table = function (header: any, body: any) {
return '' + header + body + '
'
}
highlight.configure({useBR: true});
marked.setOptions({
renderer: this.rendererMD,
headerIds: false,
gfm: true,
// tables: true,
breaks: false,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: false,
highlight: function (code: any) {
return highlight.highlightAuto(code).value;
}
});
}
async marked(data: any) {
if (data) {
let content = await marked(data);
let toc = tocObj.toHTML();
return {content: content, toc: toc};
} else {
return null;
}
}
}
const markdown: any = new MarkUtils();
export default markdown;
3.2.5 View
层
在src/views/client
下新增文件ArticleDetail.vue
文件,编写如下代码:
{{ state.detail.title }}
点赞
3.2.6 Router
层
定义route
来完成路由跳转。在src/route/index.ts
文件中新增代码:
{
path: "/article/",
name: "ArticleDetail",
component: () =>
import("../views/client/ArticleDetail.vue")
},
3.3 分类
3.3.1 Store
层
为了实现文章列表组件的复用,我们在Store
保存当前的文章的检索条件,调整后的src/store/index.ts
import {InjectionKey} from 'vue'
import {createStore, Store} from 'vuex'
import { Nav, User, ArticleParams} from "../types";
export interface State {
user: User,
navIndex: string,
navs: Array
3.3.2 View
层
通过表格查看评论,在src/views/client
下新增文件Catalog.vue
文件,编写如下代码:
3.3.5 Router
层
定义route
来完成路由跳转。在src/route/index.ts
文件中新增代码:
{
path: '/catalog',
name: 'Catalog',
component: () =>
import("../views/client/Catalog.vue")
},
3.4 归档
3.4.1 Type
层
在src/types/index.ts
文件中增加代码如下:
export interface PageInfo {
page: number,
page_size: number
}
export interface ArticleArchiveList {
year: number,
list: Array | any
}
3.4.2 API
层
列表查询,在src/api/service.ts
编写如下代码:
export function getArchiveList(params: PageInfo) {
return request({
url: '/archive/',
method: 'get',
params
})
}
3.4.3 Store
层
在src/store/index.ts
中增加如下代码:
export const SET_NAV_INDEX_BY_ROUTE = 'setNavIndexByRoute'
在src/store/index.ts
中的store
中增加如下代码:
actions: {
setNavIndexByRoute({commit, state}, route: string) {
const index = state.navs.findIndex(r => r.path === route)
if (state.navIndex === state.navs[index].index)
return
if (index > -1) {
commit(SET_NAV_INDEX, state.navs[index].index)
}
}
}
3.4.4 View
层
在src/views/client
下新增文件Archive.vue
文件,编写如下代码:
{{ l.year }}
{{ item.title }}
{{ formatTime(item.created_at) }}
3.4.5 Router
层
定义route
来完成路由跳转。在src/route/index.ts
文件中新增代码:
{
path: "/archive/",
name: "Archive",
component: () =>
import("../views/client/Archive.vue")
},
3.5 关于
3.5.1 改造index.html
微谈小智
3.5.2 View
层
通过表格查看评论,在src/views/client/ArticleDetail.vue
文件的 180~185 行增加了对about的处理文件,如下代码:
const route = useRoute()
if (route.path === '/about') {
state.params = 1
} else {
state.params = Number(route.query.id)
}
3.5.2 Router
层
定义route
来完成路由跳转。在src/route/index.ts
文件中新增代码:
{
path: '/about',
name: 'About',
component: () =>
import("../views/client/ArticleDetail.vue")
},
至此,博客针对用户的页面全部开发完成。
四、界面效果
4.1 首页
4.2 分类
4.3 归档
4.4 文章详情和关于
五、项目代码
项目的代码按照章节的代码进行的提交,能从提交记录中看到每一个模块是如何添加进去的。
个人博客地址:微谈小智 (longair.cn)
前端代码地址:https://gitee.com/Zhou_Jimmy/blog-frontend.git
后端代码地址:https://gitee.com/Zhou_Jimmy/blog-backend.git