七、文章详情
创建组件并配置路由
1、创建 views/article/index.vue
组件
<template> <div class="article-container">文章详情</div> </template>
<script> export default { name: 'ArticleIndex', components: {}, props: { articleId: { type: [Number, String], required: true } }, data () { return {} }, computed: {}, watch: {}, created () {}, mounted () {}, methods: {} } </script>
<style scoped lang="less"></style>
|
2、然后将该页面配置到根级路由
{ path: '/article/:articleId', name: 'article', component: () => import('@/views/article'), props: true }
|
官方文档:路由 props 传参
页面布局
使用到的 Vant 中的组件:
<template> <div class="article-container"> <van-nav-bar class="page-nav-bar" left-arrow title="黑马头条" ></van-nav-bar>
<div class="main-wrap"> <div class="loading-wrap"> <van-loading color="#3296fa" vertical >加载中</van-loading> </div>
<div class="article-detail"> <h1 class="article-title">这是文章标题</h1>
<van-cell class="user-info" center :border="false"> <van-image class="avatar" slot="icon" round fit="cover" src="https://img.yzcdn.cn/vant/cat.jpeg" /> <div slot="title" class="user-name">黑马头条号</div> <div slot="label" class="publish-date">14小时前</div> <van-button class="follow-btn" type="info" color="#3296fa" round size="small" icon="plus" >关注</van-button>
</van-cell>
<div class="article-content">这是文章内容</div> <van-divider>正文结束</van-divider> </div>
<div class="error-wrap"> <van-icon name="failure" /> <p class="text">该资源不存在或已删除!</p> </div>
<div class="error-wrap"> <van-icon name="failure" /> <p class="text">内容加载失败!</p> <van-button class="retry-btn">点击重试</van-button> </div> </div>
<div class="article-bottom"> <van-button class="comment-btn" type="default" round size="small" >写评论</van-button> <van-icon name="comment-o" info="123" color="#777" /> <van-icon color="#777" name="star-o" /> <van-icon color="#777" name="good-job-o" /> <van-icon name="share" color="#777777"></van-icon> </div> </div> </template>
<script> export default { name: 'ArticleIndex', components: {}, props: { articleId: { type: [Number, String], required: true } }, data () { return {} }, computed: {}, watch: {}, created () {}, mounted () {}, methods: {} } </script>
<style scoped lang="less"> .article-container { .main-wrap { position: fixed; left: 0; right: 0; top: 92px; bottom: 88px; overflow-y: scroll; background-color: #fff; } .article-detail { .article-title { font-size: 40px; padding: 50px 32px; margin: 0; color: #3a3a3a; }
.user-info { padding: 0 32px; .avatar { width: 70px; height: 70px; margin-right: 17px; } .van-cell__label { margin-top: 0; } .user-name { font-size: 24px; color: #3a3a3a; } .publish-date { font-size: 23px; color: #b7b7b7; } .follow-btn { width: 170px; height: 58px; } }
.article-content { padding: 55px 32px; /deep/ p { text-align: justify; } } }
.loading-wrap { padding: 200px 32px; display: flex; align-items: center; justify-content: center; background-color: #fff; }
.error-wrap { padding: 200px 32px; display: flex; flex-direction: column; align-items: center; justify-content: center; background-color: #fff; .van-icon { font-size: 122px; color: #b4b4b4; } .text { font-size: 30px; color: #666666; margin: 33px 0 46px; } .retry-btn { width: 280px; height: 70px; line-height: 70px; border: 1px solid #c3c3c3; font-size: 30px; color: #666666; } }
.article-bottom { position: fixed; left: 0; right: 0; bottom: 0; display: flex; justify-content: space-around; align-items: center; box-sizing: border-box; height: 88px; border-top: 1px solid #d8d8d8; background-color: #fff; .comment-btn { width: 282px; height: 46px; border: 2px solid #eeeeee; font-size: 30px; line-height: 46px; color: #a7a7a7; } .van-icon { font-size: 40px; .van-info { font-size: 16px; background-color: #e22829; } } } } </style>
|
关于后端返回数据中的大数字问题
之所以请求文章详情返回 404 是因为我们请求发送的文章 ID (article.art_id)不正确。
JavaScript 能够准确表示的整数范围在-2^53
到2^53
之间(不含两个端点),超过这个范围,无法精确表示这个值,这使得 JavaScript 不适合进行科学和金融方面的精确计算。
Math.pow(2, 53)
9007199254740992 9007199254740993
Math.pow(2, 53) === Math.pow(2, 53) + 1
|
上面代码中,超出 2 的 53 次方之后,一个数就不精确了。
ES6 引入了Number.MAX_SAFE_INTEGER
和Number.MIN_SAFE_INTEGER
这两个常量,用来表示这个范围的上下限。
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
Number.MAX_SAFE_INTEGER === 9007199254740991
Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
Number.MIN_SAFE_INTEGER === -9007199254740991
|
上面代码中,可以看到 JavaScript 能够精确表示的极限。
后端返回的数据一般都是 JSON 格式的字符串。
'{ "id": 9007199254740995, "name": "Jack", "age": 18 }'
|
如果这个字符不做任何处理,你能方便的获取到字符串中的指定数据吗?非常麻烦。所以我们要把它转换为 JavaScript 对象来使用就很方便了。
幸运的是 axios 为了方便我们使用数据,它会在内部使用 JSON.parse()
把后端返回的数据转为 JavaScript 对象。
JSON.parse('{ "id": 9007199254740995, "name": "Jack", "age": 18 }')
|
可以看到,超出安全整数范围的 id 无法精确表示,这个问题并不是 axios 的错。
了解了什么是大整数的概念,接下来的问题是如何解决?
json-bigint 是一个第三方包,它可以帮我们很好的处理这个问题。
使用它的第一步就是把它安装到你的项目中。
下面是使用它的一个简单示例。
const jsonStr = '{ "art_id": 1245953273786007552 }'
console.log(JSON.parse(jsonStr))
console.log(JSONBig.parse(jsonStr))
console.log(JSONBig.parse(jsonStr).art_id.toString())
console.log(JSON.stringify(JSONBig.parse(jsonStr)))
console.log(JSONBig.stringify(JSONBig.parse(jsonStr)))
|
json-bigint 会把超出 JS 安全整数范围的数字转为一个 BigNumber 类型的对象,对象数据是它内部的一个算法处理之后的,我们要做的就是在使用的时候转为字符串来使用。
通过 Axios 请求得到的数据都是 Axios 处理(JSON.parse)之后的,我们应该在 Axios 执行处理之前手动使用 json-bigint 来解析处理。Axios 提供了自定义处理原始后端返回数据的 API:transformResponse
。
import axios from 'axios'
import jsonBig from 'json-bigint'
var json = '{ "value" : 9223372036854775807, "v2": 123 }'
console.log(jsonBig.parse(json))
const request = axios.create({ baseURL: 'http://ttapi.research.itcast.cn/',
transformResponse: [function (data) { try { return jsonBig.parse(data) } catch (err) { return { data } } }] })
export default request
|
扩展:ES2020 BigInt
ES2020 引入了一种新的数据类型 BigInt(大整数),来解决这个问题。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。
参考链接:
展示文章详情
思路:
- 找到数据接口
- 封装请求方法
- 请求获取数据
- 模板绑定
一、请求并展示文章详情
1、在 api/article.js
中新增封装接口方法
export const getArticleById = articleId => { return request({ method: 'GET', url: `/app/v1_0/articles/${articleId}` }) }
|
2、在组件中调用获取文章详情
+ import { getArticleById } from '@/api/article'
export default { name: 'ArticlePage', components: {}, props: { articleId: { type: String, required: true } }, data () { return { + article: {} } }, computed: {}, watch: {}, created () { + this.loadArticle() }, mounted () {}, methods: { +++ async loadArticle () { try { const { data } = await getArticleById(this.articleId) this.article = data.data } catch (err) { console.log(err) } } } }
|
3、模板绑定
处理内容加载状态
需求:
- 加载中,显示 loading
- 加载成功,显示文章详情
- 加载失败,显示错误提示
- 如果 404,提示资源不存在
- 其它的,提示加载失败,用户可以点击重试重新加载
关于文章正文的样式
文章正文包括各种数据:段落、标题、列表、链接、图片、视频等资源。
图片点击预览
一、ImagePreview 图片预览 的使用
二、处理图片点击预览
思路:
1、从文章内容中获取到所有的 img DOM 节点
2、获取文章内容中所有的图片地址
3、遍历所有 img 节点,给每个节点注册点击事件
4、在 img 点击事件处理函数中,调用 ImagePreview 预览
关注用户
思路:
下面是具体实现。
视图处理
功能处理
1、在 api/user.js
中添加封装请求方法
export const addFollow = userId => { return request({ method: 'POST', url: '/app/v1_0/user/followings', data: { target: userId } }) }
export const deleteFollow = userId => { return request({ method: 'DELETE', url: `/app/v1_0/user/followings/${userId}` }) }
|
2、给关注/取消关注按钮注册点击事件
3、在事件处理函数中
import { addFollow, deleteFollow } from '@/api/user'
|
async onFollow () { this.isFollowLoading = true
try { const authorId = this.article.aut_id if (this.article.is_followed) { await deleteFollow(authorId) } else { await addFollow(authorId) }
this.article.is_followed = !this.article.is_followed } catch (err) { console.log(err) this.$toast.fail('操作失败') }
this.isFollowLoading = false }
|
最后测试。
loading 效果
两个作用:
- 交互反馈
- 防止网络慢用户多次点击按钮导致重复触发点击事件
组件封装
文章收藏
该功能和关注用户的处理思路几乎一样,建议由学员自己编写。
封装组件
处理视图
功能处理
思路:
- 给收藏按钮注册点击事件
- 如果已经收藏了,则取消收藏
- 如果没有收藏,则添加收藏
下面是具体实现。
1、在 api/article.js
添加封装数据接口
export const addCollect = target => { return request({ method: 'POST', url: '/app/v1_0/article/collections', data: { target } }) }
export const deleteCollect = target => { return request({ method: 'DELETE', url: `/app/v1_0/article/collections/${target}` }) }
|
2、给收藏按钮注册点击事件
3、处理函数
async onCollect () { this.$toast.loading({ duration: 0, message: '操作中...', forbidClick: true })
try { if (this.article.is_collected) { await deleteCollect(this.articleId) this.$toast.success('取消收藏') } else { await addCollect(this.articleId) this.$toast.success('收藏成功') } this.article.is_collected = !this.article.is_collected } catch (err) { console.log(err) this.$toast.fail('操作失败') } }
|
文章点赞
该功能和关注用户的处理思路几乎一样,建议由学员自己编写。
article 中的 attitude
表示用户对文章的态度
思路:
- 给点赞按钮注册点击事件
- 如果已经点赞,则请求取消点赞
- 如果没有点赞,则请求点赞
1、添加封装数据接口
export const addLike = articleId => { return request({ method: 'POST', url: '/app/v1_0/article/likings', data: { target: articleId } }) }
export const deleteLike = articleId => { return request({ method: 'DELETE', url: `/app/v1_0/article/likings/${articleId}` }) }
|
2、给点赞按钮注册点击事件
3、处理函数
async onLike () { this.$toast.loading({ duration: 0, message: '操作中...', forbidClick: true })
try { if (this.article.attitude === 1) { await deleteLike(this.articleId) this.article.attitude = -1 this.$toast.success('取消点赞') } else { await addLike(this.articleId) this.article.attitude = 1 this.$toast.success('点赞成功') } } catch (err) { console.log(err) this.$toast.fail('操作失败') } }
|