Appearance
Nuxt综合案例 案例
案例功能介绍
- 名称:RealWorld
- 一个开源的学习项目,目的是帮助开发者快速学习新技能,这里面可以登录,看别人的文章,点赞,评论,还可以自己发布文章,个人中心可以管理个人资料,还可以查看自己的文章和自己点赞收藏的文章。这个项目说大不大说小不小,但是可以通过这个案例学习用不同的技术实现他来熟悉一个技术栈。
- 有前端的技术栈也有后端的技术栈,在实战中体会价值。
- 还提供了页面模板和api文档,这样可以让我们忽略页面样式,专注于我们要学习的技术栈。他有服务器,是开源的,在国外可能使用会有些慢。
案例相关资源
学习前提
- Vue.js使用经验
- Nuxt.js基础
- Node.js、webpack相关使用经验
学习目标
- 掌握使用Nuxt.js开发同构渲染应用
- 增强Vue.js实践能力
- 掌握同构渲染应用中常见的功能处理
- 用户状态管理
- 页面访问权限处理
- SEO优化
- ...
- 掌握同构渲染应用的发布和部署
项目初始化
创建项目
- 创建文件夹
bash
# 创建项目目录
mkdir realworld-nuxtjs
# 进入项目目录
cd realworld-nuxtjs
# 生成 package.json 文件
npm init -y
# 安装 nuxt 依赖
npm install nuxt
- 在 package.json 中添加启动脚本:
bash
"scripts": { "dev": "nuxt" }
- 创建 pages/index.vue :
html
<template>
<div class="home">首页</div>
</template>
<script>
export default {
name: 'HomePage',
components: {},
props: {},
data () {
return {}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {}
}
</script>
<style> </style>
- 启动服务:
bash
npm run dev
- 在浏览器中访问 http://localhost:3000/ 测试。
导入页面模板
- 导入样式资源
- 配置布局组件
- 配置页面组件
导入资源样式
由于样式是国外的源,所以可以从cdn上找到中国的站点加载。
- ionicons-2.0.1版本:https://cdn.jsdelivr.net/npm/ionicons@2.0.1/css/ionicons.min.css
- googleFonts:在国内被支持
- css:http://demo.productionready.io/main.css, 全选粘贴下载到本地
static/index.css
文件
- 根目录下创建app.html,然后将模块写进去
html
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
<head {{ HEAD_ATTRS }}>
{{ HEAD }}
<!-- 字体图标 -->
<link href="https://cdn.jsdelivr.net/npm/ionicons@2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css">
<!-- google字体文件 -->
<link href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic" rel="stylesheet" type="text/css">
<!-- css文件 -->
<link rel="stylesheet" href="/index.css">
</head>
<body {{ BODY_ATTRS }}>
{{ APP }}
</body>
</html>
配置布局组件
- 创建
pages/layout/index.vue
,里面写将中的导航栏和底部栏都粘贴进去
html
<template>
<div>
<!-- nav -->
<nav class="navbar navbar-light">
<div class="container">
...
</ul>
</div>
</nav>
<!-- /nav end -->
<!-- 子路由 -->
<nuxt-child/>
<!-- / 子路由 -->
<!-- footer -->
<footer>
...
</footer>
<!-- /footer end -->
</div>
</template>
<script>
export default {
name: 'LayoutPage'
}
</script>
<style> </style>
- 重启服务可以看到页面的上下部分已经出现
配置页面组件
nuxt的路由规则,如果是大型项目,可能有的时候需要自己的结构,原来的会阻碍功能,所以这里要自己配置路由规则,不要其原始的目录路由规则。
- 在根目录下创建文件
nuxt.config.js
,里面进行路由的重新配置
js
module.exports = {
router: {
// 自定义路由表规则
extendRoutes(routes, resolve) {
// 清除 Nuxt.js 基于 pages 目录默认生成的路由表规则
routes.splice(0)
// 然后添加自己的路由对象
routes.push(...[
{
path: '/',// 根目录
component: resolve(__dirname, 'pages/layout/'),
children: [
{
path: '', // 默认子路由
name: 'home',
component: resolve(__dirname, 'pages/home/')
}
]
}
])
}
}
}
- 创建
pages/home/index.vue
,然后把给的页面模板的Home文件粘贴进去
html
<template>
<div class="home-page">
<div class="banner">
...
</div>
<div class="container page">
...
</div>
</div>
</template>
- 这个时候可以看到首页已经出来了,
导入登录注册页面
- 创建
pages/login/index.vue
,然后将页面模板中的登录注册页面粘贴进去
html
<template>
<div class="auth-page">
<div class="container page">
<div class="row">
...
</div>
</div>
</div>
</template>
- 去
nuxt.config.js
配置路由
js
routes.push(...[
{
path: '/',
component: resolve(__dirname, 'pages/layout/'),
children: [
...
{
path: '/login',
name: 'login',
component: resolve(__dirname, 'pages/login/')
},
{
path: '/register',// 注册和登录一个组件,这里单独配置一个路由
name: 'register',
component: resolve(__dirname, 'pages/login/')
}
]
}
])
- 现在要根据不同的路由展示不一样的页面,在
pages/login/index.vue
中添加计算属性isLogin,可以看到如果路由是login就是true,如果是register就是false
js
computed: {
isLogin () {
// 用name比path更合适,因为名字是自己起的
return this.$route.name === 'login'
}
}
- 在组件上进行文案的区分
- 展示大标题做区分
- 跳转登录/注册换成nuxt-link,添加v-if并且指定跳转链接
- 表格中name这一栏在注册界面才有
- 按钮的分案做区分
html
<template>
...
<div class="col-md-6 offset-md-3 col-xs-12">
<!-- 1. 展示大标题 -->
<h1 class="text-xs-center">{{ isLogin ? 'Sign in' : 'Sign up' }}</h1>
<!-- 2. 跳转登录/注册 -->
<p class="text-xs-center">
<nuxt-link v-if="!isLogin" to="/login">Have an account?</nuxt-link>
<nuxt-link v-else to="/register">Need an account?</nuxt-link>
</p>
...
<form>
<!-- 3. name这一栏在注册页面才有 -->
<fieldset v-if="!isLogin" class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Your Name">
</fieldset>
...
<!-- 4. 底部button区别文案 -->
<button class="btn btn-lg btn-primary pull-xs-right">
{{ isLogin ? 'Sign in' : 'Sign up' }}
</button>
</form>
</div>
...
</template>
- 打开浏览器访问
http://localhost:3000/login
,可以看到两个页面的不同
导入个人中心展示页面
- 创建
pages/profile/index.vue
,然后将页面模板中的profile页面粘贴进去
html
<template>
<div class="profile-page">
<div class="user-info">
...
</div>
<div class="container">
...
</div>
</div>
</template>
- 去
nuxt.config.js
配置路由
js
routes.push(...[
{
path: '/',
component: resolve(__dirname, 'pages/layout/'),
children: [
...
{
path: '/profile/:username',
name: 'profile',
component: resolve(__dirname, 'pages/profile/')
}
]
}
])
导入设置页面
- 创建
pages/settings/index.vue
,然后将页面模板中的settings页面粘贴进去
html
<template>
<div class="settings-page">
<div class="container page">
...
</div>
</div>
</template>
- 去
nuxt.config.js
配置路由
js
routes.push(...[
{
path: '/',
component: resolve(__dirname, 'pages/layout/'),
children: [
...
{
path: '/settings',
name: 'settings',
component: resolve(__dirname, 'pages/settings/')
}
]
}
])
导入创建和编辑文章
- 创建
pages/editor/index.vue
,然后将页面模板中的editor页面粘贴进去
html
<template>
<div class="editor-page">
<div class="container page">
...
</div>
</div>
</template>
- 去
nuxt.config.js
配置路由
js
routes.push(...[
{
path: '/',
component: resolve(__dirname, 'pages/layout/'),
children: [
...
{
path: '/editor',
name: 'editor',
component: resolve(__dirname, 'pages/editor/')
}
]
}
])
导入文章列表模板
- 创建
pages/article/index.vue
,然后将页面模板中的article页面粘贴进去
html
<template>
<div class="article-page">
<div class="banner">
...
</div>
<div class="container page">
...
</div>
</div>
</template>
- 去
nuxt.config.js
配置路由
js
routes.push(...[
{
path: '/',
component: resolve(__dirname, 'pages/layout/'),
children: [
...
{
path: '/article/:slug', // 动态路由,slug是链接
name: 'article',
component: resolve(__dirname, 'pages/article/')
}
]
}
])
处理顶部链接跳转
在layout/index.vue
中,将a链接转换成nuxt-link,并且把href都改成to,添加跳转的路由地址,之后再添加一个登录sign in的超链接,还有一个登录完成之后显示头像和姓名的超链接。
html
<template>
<div>
<nav class="navbar navbar-light">
<div class="container">
<!--跳到首页-->
<nuxt-link
class="navbar-brand"
to="/"
>conduit</nuxt-link>
<ul class="nav navbar-nav pull-xs-right">
<li class="nav-item">
<!--跳到首页-->
<nuxt-link class="nav-link active" to="/">Home</nuxt-link>
</li>
<li class="nav-item">
<!--跳到编辑页面-->
<nuxt-link class="nav-link" to="/editor"><i class="ion-compose"></i> New Post</nuxt-link>
</li>
<li class="nav-item">
<!--跳到设置页面-->
<nuxt-link class="nav-link" to="/settings">
<i class="ion-gear-a"></i> Settings
</nuxt-link>
</li>
<!--新加:跳到登录页面-->
<li class="nav-item">
<nuxt-link class="nav-link" to="/login">Sign in</nuxt-link>
</li>
<li class="nav-item">
<!--跳到注册页面-->
<nuxt-link class="nav-link" to="/register">Sign up</nuxt-link>
</li>
<!--新加:登录之后显示用户头像和名称-->
<li class="nav-item">
<nuxt-link class="nav-link" to="/profile/112">
<img class="user-pic" src="https://tvax4.sinaimg.cn/crop.0.0.512.512.1024/001nd69sly8gjfgoc7b8dj60e80e8q3a02.jpg?KID=imgbed,tva&Expires=1609784063&ssig=EQg2ckyhVH">
aibugi111
</nuxt-link>
</li>
</ul>
</div>
</nav>
<!-- /nav end -->
</template>
处理导航添加高亮
- 在点击导航栏的时候,点击别的地方并没有显示高亮,在路由配置的地方,如果点击跳转路由,对应的
<nuxt-link>
中会有类名nuxt-link-active,这个是高亮的类,那么这个可以在nuxt.config.js
的router中进行配置,改为active
js
module.exports = {
router: {
// 自定义高亮类名,默认nuxt-link-active
linkActiveClass: 'active',
...
}
}
- 这个时候有高亮效果了,但是就算我们把active去掉Home键还是一直高亮,是因为匹配的规则中,是包含关系并不是精确匹配,要做到精确匹配,在
'/'
中添加属性exact,就可以实现精确匹配,不会常亮。
html
<!--layout/index.vue-->
<nuxt-link
class="nav-link"
to="/"
exact
>Home</nuxt-link>
- 高亮设置完成
封装请求模块
- 安装axios,
npm i axios --save
- 根目录下创建
utils/request.js
,然后写请求模块架构
js
import axios from 'axios'
const request = axios.create({
// 在页面模板中有设置基准路径,后面跟api
baseURL: 'https://conduit.productionready.io'
})
// 请求拦截器
// 相应拦截器
export default request
登录注册
实现基本登录功能
- 去接口文档中找到登录接口,需要的是email和password,而且都是必须的。
- 在
login/index.vue
文件中定义data
js
data () {
return {
user: {
email: '',
password: ''
}
}
}
- 在template中的email和password中加入监听,并设置required成为必填项,email添加格式验证(原生的有兼容性问题,这里不是重点不做处理),密码不少于8位。
html
<!-- email -->
<fieldset class="form-group">
<input
v-model="user.email"
class="form-control form-control-lg"
type="email"
placeholder="Email"
required
>
</fieldset>
<!-- password -->
<fieldset class="form-group">
<input
v-model="user.password"
class="form-control form-control-lg"
type="password"
placeholder="Password"
required
minlength="8"
>
</fieldset>
- 在from表单中绑定submit事件,并且将默认的提交操作阻止,绑定自己的事件
html
<form @submit.prevent="onSubmit">
...
</form>
- 在methods中定义提交事件,获取data,并且跳到首页
js
methods: {
async onSubmit () {
// 提交表单请求扽牢固
const { data } = await request({
method: 'POST',
url: '/api/users/login',
data: {
user: this.user
}
})
console.log(data)
// 保存用户的登录状态
// 跳转到首页
this.$router.push('/')
}
}
- 演示一下登录接口确实拿到了数据
js
{
"user": {
"id": 133697,
"email": "shuangfeng1993@163.com",
"createdAt": "2021-01-06T06:20:22.385Z",
"updatedAt": "2021-01-06T06:20:22.392Z",
"username": "hu77163",
"bio": null,
"image": null,
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTMzNjk3LCJ1c2VybmFtZSI6Imh1NzcxNjMiLCJleHAiOjE2MTUwOTgwMjJ9.0W_dJZNpLXfI0Q5bsLz-qAqRsJCVq3yVpEOWFwnBMy0"
}
}
- 下面将api的请求进行一下优化,根目录下创建
api/user.js
js
import request from '@/utils/request'
// 用户登录
export const login = data => {
return request({
method: 'POST',
url: '/api/users/login',
data
})
}
// 用户注册
export const register = data => {
return request({
method: 'POST',
url: '/api/users',
data
})
}
- 在
login/index.vue
中修改
js
async onSubmit () {
// 提交表单请求扽牢固
const { data } = await login({
user: this.user
})
...
}
错误处理
- 在
login/index.vue
中将try-catch把提交表单的部分包裹起来,然后把error信息用console.dir(error)
打印出来。
- 分析error,我们打算将key和数组一起展示,数组中有几条就展示几条,那么先对errors进行对象遍历,然后对值进行数组遍历,将键与值合并展示。
html
<ul class="error-messages">
<!-- 先将errors对象进行遍历,massages是数组,也进行遍历,将key在前,value在后 -->
<template
v-for="(messages, field) in errors">
<li
v-for="(message, index) in messages"
:key="index"
>
{{ field }} {{ massage }}
</li>
</template>
</ul>
- 添加errors字段
js
data () {
return {
...
errors: {} // 错误信息,格式是key:value[]
}
}
...
async onSubmit () {
try {
...
} catch (error) {
console.dir(error)
this.errors = error.response.data.errors
}
}
实现用户注册
- 在
login/index.vue
中添加username字段
html
<fieldset v-if="!isLogin" class="form-group">
<!--绑定username,设置不能缺少-->
<input v-model="user.username" required class="form-control form-control-lg" type="text" placeholder="Your Name">
</fieldset>
js
// 把登录和注册引入
import { login, register } from '@/api/user'
...
data () {
return {
user: {
// 添加username字段
username: '',
...
}
...
}
}
- 提交的时候判断登录还是注册
js
async onSubmit () {
try {
// 提交表单请求登录或者注册
const { data } = this.isLogin
? await login({
user: this.user
})
: await register({
user: this.user
})
console.log(data)
// 保存用户的登录状态
// 跳转到首页
this.$router.push('/')
} catch (error) {
console.dir(error)
this.errors = error.response.data.errors
}
}
保存登录状态
- Nuxt官方示例中的保存登录状态示例
- 因为登录状态的存储需要多个页面共享,也需要前后端共享,所以这里会用到Vuex,在nuxt中已经集成了Vuex。
- process.client和process.server判断环境
- process.client在客户端为true,在服务端运行为false
- process.server 在服务端运行为true,在客户端运行为false
- 这个对象是在nuxt中特殊提供的对象,同来判断环境
- nuxtServerInit是store中用到的服务端特殊的方法,详情见 nuxtServerInit 方法
将登录状态存储到容器中
- 根目录下创建
store/index.js
,在里面将state和mutation和action定义之后导出,nuxt会去加载。
必须要叫这个目录,nuxt发现store,会自动加载容器模块。
js
// 在服务端渲染期间运行的都是同一个实例
// 为了防止数据冲突,务必把state定义成一个函数,返回数据对象
export const state = () => {
return {
// 当前登录用户的登录状态
user: null
}
}
export const mutations = {
setUser (state, data) {
state.user = data
}
}
export const actions = {
}
- 去
login/index.vue
中存储登录状态
js
async onSubmit () {
try {
...
// 保存用户的登录状态
// 1. 将程序运行期间的状态先存在内存中
this.$store.commit('setUser', data.user)
...
} catch (error) {
...
}
}
登录状态持久化
- 在获取到登录状态的时候进行存储,先安装
npm i js-cookie --save-dev
- 在
login/index.vue
中使用
js
// 判断环境按需引入
const Cookie = process.client ? require('js-cookie') : undefined
js
async onSubmit () {
try {
...
// 保存用户的登录状态
// 1. 将程序运行期间的状态先存在内存中
this.$store.commit('setUser', data.user)
// 2. 为了防止刷新页面数据丢失,将状态持久化,放入cookie里,这个方法会将数据进行编码转成字符串放在cookie里
Cookie.set('user', data.user)
...
} catch (error) {
...
}
}
- 安装后端渲染的包
npm i cookieparser --save-dev
- 在页面初始化的时候,从服务端存入数据
store/index.js
,先引入需要的包
js
// 后端渲染需要的包
const cookieparser = process.server ? require('cookieparser') : undefined
- 在下面导入actions,然后将nuxtServerInit方法写入
js
export const actions = {
// nuxt中特殊的action方法
// 这个 action 方法会在服务端渲染期间自动调用,且仅在服务端调用
// 作用:初始化容器数据,传递数据给客户端使用
nuxtServerInit ({ commit }, { req }) {
let user = null
// 如果请求头中有 Cookie
if (req.headers.cookie) {
// 使用 cookieparser 把 cookie 字符串转为 json 对象
const parsed = cookieparser.parse(req.headers.cookie)
// 如果存的不是json字符串,避免报错
try{
user = JSON.parse(parsed.user)
} catch (err) {
// No valid cookie found
// 失败不用做处理,还是null
}
}
// 提交mutation修改state状态
commit('setUser', user)
}
}
导航栏页面状态区分、
- 去导航栏页面
layout/index.vue
将登录未登录的地方进行显示区分
html
<ul class="nav navbar-nav pull-xs-right">
<!--首页导航不需要登录状态-->
<li class="nav-item">
<nuxt-link class="nav-link" to="/" exact>Home</nuxt-link>
</li>
<!--需要登录状态的用template进行包裹,判断条件是user-->
<template v-if="user">
<li class="nav-item">
<nuxt-link class="nav-link" to="/editor"><i class="ion-compose"></i> New Post</nuxt-link>
</li>
<li class="nav-item">
<nuxt-link class="nav-link" to="/settings">
<i class="ion-gear-a"></i> Settings
</nuxt-link>
</li>
<li class="nav-item">
<!--这里将用户名和头像进行展示-->
<nuxt-link class="nav-link" to="/profile/112">
<img class="user-pic" :src="user.image? user.image : defaultImage">
{{ user.username }}
</nuxt-link>
</li>
</template>
<!--未登录状态下展示的内容,也有template包裹-->
<template v-else>
<li class="nav-item">
<nuxt-link class="nav-link" to="/login">Sign in</nuxt-link>
</li>
<li class="nav-item">
<nuxt-link class="nav-link" to="/register">Sign up</nuxt-link>
</li>
</template>
</ul>
- 将登录状态进行区分
html
<script>
// 将mapState导入
import { mapState } from 'vuex'
export default {
name: 'LayoutPage',
computed: {
...mapState(['user'])
},
data () {
return {
defaultImage: 'https://tvax4.sinaimg.cn/crop.0.0.512.512.1024/001nd69sly8gjfgoc7b8dj60e80e8q3a02.jpg?KID=imgbed,tva&Expires=1609784063&ssig=EQg2ckyhVH'
}
}
}
</script>
- 这个时候重新启动服务,登录之后可以看到导航栏区别和用户名头像的展示。
处理页面访问权限
未登录不能访问的页面
虽然上面处理了不让直接访问editor链接,但是通过地址栏输入,依旧可以访问编辑文章等页面,这里就需要给页面添加访问权限。
TIP
如果是Vue的话,可以通过使用路由拦截器的方式,现在在Nuxt里面可以换一种方式。他提供了一种叫 路由中间件 的方式,既能处理服务端路由拦截,也可以处理客户端路由拦截。
- 在根目录下创建
middleware/authenticated.js
文件,然后在里面进行处理
js
/**
* 验证是否登录的中间件
*/
// 从上下文中获取store对象和redirect方法
export default function ({ store, redirect }) {
// 判断如果没有user,就是没有登录
if(!store.state.user) {
// 跳转到登录页面
return redirect('/login')
}
}
- 在需要的页面(profile,settings,editor)中添加属性
js
export default {
// 在路由匹配组件渲染之前会先执行中间件处理
// 如果有一个中间件写字符串里面,如果有多个中间件写数组里面
// 这里的值就是中间件的js文件名,会找到这个文件然后调用
middleware: 'authenticated',
name: 'EditorPage'
}
- 重启服务之后退出登录,访问
http://localhost:3000/editor
就会自动跳转到登录页面。
登录之后限制访问的页面
如果登录之后,那么登录和注册的页面也不能访问,这里也进行处理。
- 在middleware文件夹中添加
noauthenticated.js
文件
js
// 从上下文中获取store对象和redirect方法
export default function ({ store, redirect }) {
// 判断如果有user,就是有登录
if(store.state.user) {
// 跳转到首页
return redirect('/')
}
}
- 在
login/index.vue
中注册
js
export default {
middleware: 'noauthenticated',
name: 'LoginIndex',
...
}
- 这个时候登录后的登录页面直接返回首页
首页
业务介绍
- 文章列表
- 用户关注文章列表(登录之后才可见)
- 所有文章列表
- 文章分类筛选(点击全部消失)
- 分页
展示公共文章列表
- api找 List Articles,这里面是文章列表,创建
api/article.js
,在里面写接口
js
import request from '@/utils/request'
// 获取公共文章列表
export const getArticles = params => {
return request({
method: 'GET',
url: '/api/articles',
params
})
}
- 在
home/index.vue
中请求数据,这里使用后端渲染,所以写在asyncData中
js
async asyncData() {
const {data} = await getArticles()
console.log(data)
// 看下面的数据结构,然后将articles和articlesCount解构出来
return {
articles: data.articles,
articlesCount: data.articlesCount
}
}
- 可以看到打印出来的数据是
js
{
"articles": [
{
// 标题
"title": "12",
// 文章唯一标识
"slug": "12-hbwkz7",
// 内容
"body": "ww",
// 文章创建时间
"createdAt": "2021-01-06T12:09:58.098Z",
// 文章更新时间
"updatedAt": "2021-01-06T12:09:58.098Z",
// 标签
"tagList": [],
// 描述
"description": "32",
// 作者列表
"author": [
null
],
// 是否点赞过
"favorited": false,
// 一共几个点赞信息
"favoritesCount": 0
}
...
],
// 文章总数
"articlesCount": 500
}
- 在这页上面进行列表渲染
html
<!--文章列表内容,对articles进行遍历,key就是slug-->
<div
class="article-preview"
v-for="article in articles"
:key="article.slug"
>
<div class="article-meta">
<!--用户头像,把a链接改为nuxt-link,跳转到这个用户的主页to里面添加params-->
<nuxt-link
:to="{
name:'profile',
params: {
username: articles.author.username
}
}">
<img :src="article.author.image" /></nuxt-link>
<!--用户名称,a链接改为nuxt-link,跳转到这个用户的主页-->
<div class="info">
<nuxt-link
class="author"
:to="{
name:'profile',
params: {
username: articles.author.username
}
}">
{{ article.author.username }}</nuxt-link>
<!--文章创建时间-->
<span class="date">{{ article.createdAt }}</span>
</div>
<!--文章点赞数量,添加动态样式自己是否给她点过赞-->
<button
class="btn btn-outline-primary btn-sm pull-xs-right"
:class="{
active: article.favorited
}">
<i class="ion-heart"></i> {{ article.favoritesCount }}
</button>
</div>
<!--a链接替换,这里链接到文章详情-->
<nuxt-link
:to="{
name: 'article',
params: {
slug: article.slug
}
}"
class="preview-link"
>
<!--文章标题和描述-->
<h1>{{ article.title }}</h1>
<p>{{ article.description }}</p>
<span>Read more...</span>
</nuxt-link>
</div>
- 之后可以看到首页有数据渲染出来。
分页功能
这里要处理两部分内容:
- 页码对应的数据处理
- 和用户的交互
在文章列表中有两个参数,一个是limit(每次请求文章的条数),一个是offset(默认是0,文章偏移量,如果值为2表示跳过前两条取后面的)
- 现在定义url中的页面形式是
http://localhost:3000/?page=3
,每个页面从query中取,如果没有默认是1,然后传参的时候,限制为20,偏移量是(当前页-1)*偏移量
js
async asyncData({ query }) {
const page = Number.parseInt(query.page || 1)
const limit = 20
const {data} = await getArticles({
limit,
offset: (page - 1) * limit
})
console.log(data)
// 将page和limit也导出
return {
articles: data.articles,
articlesCount: data.articlesCount,
page,
limit
}
}
- 将页码从demo中copy出来
html
<nav>
<ul class="pagination">
<li class="page-item">
<a class="page-link">1</a>
</li>
</ul>
</nav>
- 下面我们求出总页数,这里使用计算属性,总页数/每页数量,向上取整即可。
js
computed: {
totalPage () {
return Math.ceil(this.articlesCount / this.limit)
}
}
- 之后在分页列表中进行修改
html
<nav>
<ul class="pagination">
<!--根据总页数进行内容遍历,然后添加动态高亮active,当页数等于当前值的时候-->
<li
class="page-item"
:class="{
active: item === page
}"
v-for="item in totalPage"
:key="item">
<!--使用nuxt-link不跳转,点击的时候给后面query添加page参数,值是当前点击的值-->
<nuxt-link
class="page-link"
:to="{
name: 'home',
query: {
page: item
}
}">{{item}}</nuxt-link>
</li>
</ul>
</nav>
- 目前这样会有问题,因为没有刷新页面所以数据不会重新获取,如果要改变query的时候也调用asyncData,那么就看这个文档
这里要在home/index.vue
页面中添加page的改变进行监听,这里修改之后必须更新,热更新有问题。
js
export default {
watchQuery: ['page']
}
- 这样点击更新就会更新页面了。
标签列表
可以根据标签,筛选显示的内容。先找到对应的接口/api/tags
- 创建
api/tag.js
,写对应接口
js
import request from '@/utils/request'
// 获取标签列表
export const getTags = () => {
return request({
method: 'GET',
url: '/api/tags'
})
}
- 在
home/index.vue
中,因为标签也需要seo,所以这里也需要在asyncData中获取数据
js
async asyncData({ query }) {
...
// 因为已经有data了,所以这里做个别名tagData
const { data: tagData } = await getTags()
console.log(tagData)
// 这里return的时候将tags返回
return {
articles: data.articles,
articlesCount: data.articlesCount,
page,
limit,
tags: tagData
}
},
- 可以看到拿到的数据,tags里面是一个数组
js
{
"tags": [
"\u200c",
"\u200c\u200c",
"\u200c\u200c\u200c",
"\u200c\u200c\u200c\u200c\u200c",
"\u200c\u200c\u200c\u200c",
"\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200c",
"\u200c\u200c\u200c\u200c\u200c\u200c\u200c",
"\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200c",
"\u200c\u200c\u200c\u200c\u200c\u200c",
"\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200c",
"HuManIty",
"Hu\u200cMan\u200cIty",
"Gandhi",
"HITLER",
"SIDA",
"BlackLivesMatter",
"Black\u200cLives\u200cMatter",
"test",
"dragons",
"butt"
]
}
- 因为获取文章列表和获取标签列表没有依赖关系,完全可以并行,使用promise.all
js
async asyncData({ query }) {
const page = Number.parseInt(query.page || 1)
const limit = 20
// Promise.all的结果,第一个执行的结果在下标为0,第二个执行的结果在下标为1
const [ articleRes, tagRes ] = await Promise.all([
getArticles({
limit,
offset: (page - 1) * limit
}),
getTags()
])
const { articles, articlesCount } = articleRes.data
const { tags } = tagData.data
return {
articles,
articlesCount,
page,
limit,
tags
}
}
- 点击标签,和分页一样,同步在query中,这里先将标签遍历,并且点击的时候跳转传参query
html
<div class="tag-list">
<nuxt-link :to="{
name: 'home',
query: {
tag: item
}
}" class="tag-pill tag-default"
v-for="item in tags"
:key="item">{{ item }}</nuxt-link>
</div>
- 这个时候将搜索文章的接口中传入
tag
参数,并且监听页面的tag query
js
export default {
...
async asyncData({ query }) {
...
// 传入tag
const [ articleRes, tagRes ] = await Promise.all([
getArticles({
limit,
offset: (page - 1) * limit,
tag: query.tag
}),
getTags()
])
...
},
// 监听tag变化
watchQuery: ['page','tag'],
...
}
- 这个时候要对页面和标签进行联动,点击tag的时候是默认第一页所以不用管,如果是有tag的时候点页数,那么需要加两个参数
html
<ul class="pagination">
<li
class="page-item"
:class="{
active: item === page
}"
v-for="item in totalPage"
:key="item">
<!--里面添加tag,值是当前的route里面的tag-->
<nuxt-link
class="page-link"
:to="{
name: 'home',
query: {
page: item,
tag: $route.query.tag
}
}">{{item}}</nuxt-link>
</li>
</ul>
文章列表导航栏
说明
- 登录状态下,有用户点赞的文章、所有文章
- 未登录状态下,只有所有文章
- 点击标签,会新增一栏标签栏文章
- 点击我点赞的文章和所有文章,标签栏文章会去掉
设计
用户点赞的文章:tab = your_feed 所有文章: tab = global_feed(默认) 标签文章: tab = tag
实现
- 首先,确定第一个用户点赞文章,是在登录状态下才展示,引入登录状态
js
import { mapState } from "vuex";
...
computed: {
...mapState(['user']),
...
}
html
<ul class="nav nav-pills outline-active">
<!-- 1.登录状态下才展示,设置v-if为user -->
<li class="nav-item" v-if="user">
<!--2. a链接修改为nuxt-link,
3. 添加动态高亮,active是tab为your_feed的时候高亮,4. 然后添加跳转,点击的时候,要跳到首页并且query的tab为you_feed
5. 因为路由只匹配/,后面的query没法匹配,所以这里要用精准匹配-->
<nuxt-link
class="nav-link"
:class="{
active: tab === 'your_feed'
}"
:to="{
name: 'home',
query: {
tab: 'your_feed'
}
}"
exact>Your Feed</nuxt-link>
</li>
...
</ul>
- 所有文章,不需要登录状态
html
<li class="nav-item">
<!--1. a标签替换为nuxt-link
2. 标签高亮,当tab为global_feed
3. 跳转的时候query后面tab变成global_feed
4. 因为路由的关系要精准匹配-->
<nuxt-link
class="nav-link"
:class="{
active: tab ==='global_feed'
}"
:to="{
name: 'home',
query: {
tab: 'global_feed'
}
}"
exact>Global Feed</nuxt-link>
</li>
- 这里要设置所有文章是默认的
js
async asyncData({ query }) {
...
return {
...
tab: query.tab || 'global_feed'
}
}
- 这里的时候,文章高亮还是不对,除了精准匹配,这里还要对tab的query进行监听
js
watchQuery: ['page','tag','tab']
- 之后就是最后一个tag的导航栏了,因为一直用到这个tag,要放在query中,还要展示在标签导航栏上,首先将query中的tag存到data中
js
async asyncData({ query }) {
...
const { tag } = query
...
return {
articles,
articlesCount,
page,
limit,
tags,
tag,
tab: query.tab || 'global_feed'
}
}
- 最后一个tag的导航栏
html
<!--1. 如果tag不存在就不展示-->
<li class="nav-item" v-if="tag">
<!--2. a链接编程nuxt-link
3. 动态高亮,tab为tag
4. 跳转,query为两个参数,一个是tab为tag,一个是tag
5. 标签的文案进行变化-->
<nuxt-link
class="nav-link"
:class="{
active: tab === 'tag'
}"
:to="{
name: 'home',
query: {
tab: 'tag',
tag: tag
}
}"
>#{{ tag }}</nuxt-link>
</li>
- 这个时候点击标签列表,标签的tab没有高亮,所以需要修改标签列表的跳转。
html
<div class="tag-list">
<nuxt-link :to="{
name: 'home',
query: {
tag: item,
tab: 'tag'
}
}" class="tag-pill tag-default"
v-for="item in tags"
:key="item">{{ item }}</nuxt-link>
</div>
- 点击分页按钮,要对当前的tab进行保持
html
<li
class="page-item"
:class="{
active: item === page
}"
v-for="item in totalPage"
:key="item">
<nuxt-link
class="page-link"
:to="{
name: 'home',
query: {
page: item,
tag: tag,
tab: tab
}
}">{{item}}</nuxt-link>
</li>
用户文章列表
- 用户文章列表需要进行改造,首先有一个api是针对这个的
js
// 获取用户文章列表
export const getFeedArticles = params => {
return request({
method: 'GET',
url: '/api/articles/feed',
params
})
}
2,之后在home/index.vue
中使用
js
// 1. 引入状态管理store
async asyncData({ query, store }) {
const page = Number.parseInt(query.page || 1)
const limit = 20
// 2. 将tab赋值到一个变量
const { tag, tab = 'global_feed' } = query
// 3. 判断登录状态,且是不是your_feed标签栏,分别设置给不同的函数
const loadArticles = store.state.user && tab === 'your_feed' ? getFeedArticles : getArticles
// 4. 调用函数获取文章列表
const [ articleRes, tagRes ] = await Promise.all([
loadArticles({
limit,
offset: (page - 1) * limit,
tag: tag
}),
getTags()
])
const { articles, articlesCount } = articleRes.data
const { tags } = tagRes.data
// 把tab进行修改
return {
articles,
articlesCount,
page,
limit,
tags,
tag,
tab
}
},
- 保存之后报错,因为我们并没有证明我们自己的身份,我们需要在请求的时候在headers里面传token参数,这个在文档中有写到。
- 这里需要用到axios的拦截器
js
// 请求拦截器
// 任何请求都要经过拦截器,可以在请求拦截器中做一些公共的业务处理,例如统一设置 token
request.interceptors.request.use(function (config) {
// 请求会经过这里
// 这里拿到的config是请求中的所有数据,先拿到请求头,然后写文档中要求的字段Authorization
config.headers.Authorization = `Token 用户token`
// 返回 config 请求配置对象
return config;
}, function (error) {
// 如果请求失败(此时请求还没有发出去)就会进入这里
// Do something with request error
return Promise.reject(error);
});
- 关键那个用户token怎么获取,我们之前使用store获取,但是现在他是用函数导出的方式,我们获取不到user,我们使用纯客户端的方式是完全不行的,这里用到了新的知识点 —— Nuxt的插件
Nuxt的插件
Nuxt.js 允许您在运行 Vue.js 应用程序之前执行 js 插件。这在您需要使用自己的库或第三方模块时特别有用。 这里我们可以拿到我们需要的状态
- 使用第三方模块,安装使用,我们这个一直在用
- Vue插件,创建plugin文件夹,然后注册到vue中,还要配置到nuxt.config.js
- ES6插件,如果是ESModules,而且在node_modules中,要添加到transpile中
- 注入$root和context
通过插件,让插件导出一个函数,在函数中就可以拿到运行期间的app根实例,或者上下文对象,包括store都在里面。都能正确拿到数据
- 创建
plugins/request.js
,用插件的方式写axios
js
import axios from 'axios'
const request = axios.create({
// 在页面模板中有设置基准路径,后面跟api
baseURL: 'https://conduit.productionready.io'
})
// 通过插件机制获取到上下文对象(query,params,req,res,app,store...)
export default (context) => {
console.log(context)
}
- 然后记得要在config中进行注册
js
module.exports = {
router: {
...
},
// 注册插件,前面要加~,从根路径出发的意思
plugins: [
'~/plugins/request.js'
]
}
- 浏览器打印可以看到输出的上下文
js
{
isStatic: false,
isDev: true,
isHMR: false,
app: {
...
},
store: Store {
...
},
payload: undefined,
error: [Function: bound
...
}
- 然后将contxt解构出来,拿到用户信息之后将request也按需导出
js
/**
* 基于 axios 封装的请求模块
*/
import axios from 'axios'
// 按需导出
export const request = axios.create({
// 在页面模板中有设置基准路径,后面跟api
baseURL: 'https://conduit.productionready.io'
})
// 插件导出函数只能用一个成员,且是默认成员
// 通过插件机制获取到上下文对象(query,params,req,res,app,store...)
export default ({ store }) => {
// // 请求拦截器
// // 任何请求都要经过拦截器,可以在请求拦截器中做一些公共的业务处理,例如统一设置 token
request.interceptors.request.use(function (config) {
// 请求会经过这里
// 这里拿到的config是请求中的所有数据,先拿到请求头,然后写文档中要求的字段Authorization
const { user } = store.state
if (user && user.token) {
config.headers.Authorization = `Token ${user.token}`
}
// 返回 config 请求配置对象
return config;
}, function (error) {
// 如果请求失败(此时请求还没有发出去)就会进入这里
// Do something with request error
return Promise.reject(error);
});
// 相应拦截器
}
- 然后我们将api文件夹中的请求文件和store的请求文件都换到plugins下面,而且要用下面的方式按需加载request
js
import { request } from '@/plugins/request'
- 打开浏览器,可以看到访问的时候正确了,因为我没有给任何人点过赞,所以这里看不到点赞信息。可以看到请求头中已经添加了token,说明生效了。
日期格式化处理
推荐一个dayjs的事件格式化类库,是moment.js的轻量化方案,为什么轻量是因为其采用模块化方式,本身只集成了最核心的方法,其他的不常用的需要的话就按需加载。
我们除了首页要使用日期格式化,还要在文章详情页使用,所以最好是做一个全局的过滤器。我们这里要用到插件。
- 安装
npm i dayjs --save
- 创建
plugins/dayjs.js
js
// 导入vue和dayjs
import Vue from 'vue'
import dayjs from 'dayjs'
// {{ 表达式 | 过滤器 }}
// 注册全局过滤器date,参数是value,第二个是format,如果用户传自己的就用自己的,不传就用默认的。
Vue.filter('date', (value, format='YYYY-MM-DD:HH:mm:ss') => {
return dayjs(value).format(format)
})
- 在nuxt.config.js中进行注册
js
plugins: [
'~/plugins/request.js',
'~/plugins/dayjs.js'
]
- 在
home/index.vue
中添加过滤器
html
<span class="date">{{ article.createdAt | date }}</span>
- 如果想要修改格式,可以直接添加参数
html
<span class="date">{{ article.createdAt | date('MMM DD, YYYY') }}</span>
文章点赞功能
- 给文章点赞,点赞按钮高亮,然后数字+1
- 取消点赞,按钮不高亮,数字-1
- 在
api/article.js
中添加接口
js
// 添加点赞
export const addFavorite = slug => {
return request({
method: 'POST',
url: `/api/articles/${slug}/favorite`
})
}
// 取消点赞
export const deleteFavorite = slug => {
return request({
method: 'DELETE',
url: `/api/articles/${slug}/favorite`
})
}
- 给按钮添加点击事件
html
<button
class="btn btn-outline-primary btn-sm pull-xs-right"
:class="{
active: article.favorited
}"
@click="onFavorite(article)">
<i class="ion-heart"></i> {{ article.favoritesCount }}
</button>
- 在methods中添加方法
js
async onFavorite (article) {
if(article.favorited) {
// 取消点赞
await deleteFavorite(article.slug)
// 处理视图
article.favorited = false
article.favoritesCount += -1
} else {
// 添加点赞
await addFavorite(article.slug)
// 处理视图
article.favorited = true
article.favoritesCount += 1
}
}
- 添加请求过程中点击禁用的功能,先在asyncData初始化的时候遍历添加一个可点状态的属性
js
async asyncData({ query, store }) {
...
const { articles, articlesCount } = articleRes.data
const { tags } = tagRes.data
// 添加可点赞状态
articles.forEach(articles => articles.favoriteDisabled = false)
return {
articles,
articlesCount,
page,
limit,
tags,
tag,
tab
}
},
- 然后在button中绑定
html
<button
class="btn btn-outline-primary btn-sm pull-xs-right"
:class="{
active: article.favorited
}"
@click="onFavorite(article)"
:disabled="article.favoriteDisabled">
<i class="ion-heart"></i> {{ article.favoritesCount }}
</button>
- 之后再methods中修改
js
async onFavorite (article) {
// 请求之前禁用点击
article.favoriteDisabled = true
if(article.favorited) {
...
} else {
...
}
// 请求之后开启点击
article.favoriteDisabled = false
}
文章详情
功能分析
- 展示文章详情
- 写评论、展示评论功能
- 头像、名称、收藏和点赞可以封装组件
展示基本信息
- api/article.js中写接口
js
// 获取文章详情
export const getArticle = slug => {
return request ({
method: 'GET',
url: `/api/articles/${slug}`
})
}
- 在
article/index.vue
中引入请求
js
import { getArticle } from "@/api/article";
export default {
name: 'ArticlePage',
async asyncData({ params }) {
// 将slug传入,返回data
const { data } = await getArticle(params.slug)
console.log(data)
}
}
看到我们返回的data中的结构,所以这里返回article
js
{
article: {
title: '1003 Lamps',
slug: '1003-lamps-v1n15n',
body: 'count',
createdAt: '2021-01-07T07:19:59.490Z',
updatedAt: '2021-01-07T07:19:59.490Z',
tagList: [...],
description: 'count',
author: {
username: 'Aprii',
bio: 'hellooo0',
image: 'https://st.quantrimang.com/photos/image/2016/11/11/anh-gif-3.gif/backGRound/https://images.unsplash.com/photo-1609270019516-73e889aafaac?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MXw3MDY2fDB8MXxjb2xsZWN0aW9ufDI1fDMxNzA5OXx8fHx8Mnw&ixlib=rb-1.2.1&q=80&w=1080',
following: false
},
favorited: false,
favoritesCount: 0
}
}
js
import { getArticle } from "@/api/article";
export default {
name: 'ArticlePage',
async asyncData({ params }) {
const { data } = await getArticle(params.slug)
// 将article返回
return {
article: data.article
}
}
}
- 修改文章标题,可以看到页面有正常的显示
html
<h1>{{ article.title }}</h1>
将正文的markdown格式转化为html格式
- 因为文章正文支持markdown格式,使用第三方包
markdown-it
,将markdown格式的转化成html格式,下面进行安装
bash
npm i markdown-it --save
- 在
article/index.vue
中引用
js
import MarkdownIt from "markdown-it";
...
async asyncData({ params }) {
console.log(params)
const { data } = await getArticle(params.slug)
const { article } = data
// 在导出之前将文章内容转化为html
const md = new MarkdownIt()
article.body = md.render(article.body)
console.log(data)
return {
article
}
}
...
这个时候可以看到body的东西有了标签展示
- html中进行引用
html
<div class="row article-content">
<div class="col-md-12" v-html="article.body"></div>
</div>
作者信息组件
- 在article文件夹中创建
components/articl-meta.js
文件,然后把模板拷贝进去。
html
<template>
<div class="article-meta">
<a href=""><img src="https://tvax4.sinaimg.cn/crop.0.0.690.690.1024/0075v9WWly8gm3jbk0ufnj30j60j6t9f.jpg?KID=imgbed,tva&Expires=1609784063&ssig=zs0nC1fpU1" /></a>
<div class="info">
<a href="" class="author">Eric Simons</a>
<span class="date">January 20th</span>
</div>
<button class="btn btn-sm btn-outline-secondary">
<i class="ion-plus-round"></i>
Follow Eric Simons <span class="counter">(10)</span>
</button>
<button class="btn btn-sm btn-outline-primary">
<i class="ion-heart"></i>
Favorite Post <span class="counter">(29)</span>
</button>
</div>
</template>
<script>
export default {
name: 'ArticleMeta'
}
</script>
<style> </style>
- 然后在index.vue中注册并使用组件,将article-mate的div换成下面的单标签,两个地方,并且将文章的数据传入。
html
<article-meta :article="article"/>
js
import ArticleMeta from './components/article-meta';
...
components: {
ArticleMeta
}
- 在article-meta.vue中进行参数的接收
js
// 确定article是对象,且必须
props: {
article: {
type: Object,
require: true
}
}
- 展示在页面上
html
<template>
<div class="article-meta">
<!-- 作者头像,点击跳转到个人中心 -->
<nuxt-link
:to="{
name: 'profile',
params: {
username: article.author.username
}
}">
<img :src="article.author.image" />
</nuxt-link>
<!-- 名称、日期 -->
<div class="info">
<!--用户名称,点击跳转到个人中心-->
<nuxt-link
:to="{
name: 'profile',
params: {
username: article.author.username
}
}"
class="author"
>
{{ article.author.username }}
</nuxt-link>
<!--日期,使用过滤器-->
<span class="date">{{ article.createdAt | date('MMM DD, YYYY') }}</span>
</div>
<!-- 关注,类名看是否关注,去掉数量(接口中没有) -->
<button
class="btn btn-sm btn-outline-secondary"
:class="{
active: article.author.following
}"
>
<i class="ion-plus-round"></i>
Follow Eric Simons
</button>
<!-- 点赞,类名看是否点赞,记录点赞数量 -->
<button
class="btn btn-sm btn-outline-primary"
:class="{
active: article.favorited
}"
>
<i class="ion-heart"></i>
Favorite Post <span class="counter">({{ article.favoritesCount }})</span>
</button>
</div>
</template>
- 这样就完成了组件的配置
设置页面meta优化SEO
我们想要文章详情的标题成为html标题,并且设置meta,如何处理?
- Nuxt视图 > HTML 头部
- 参考上面的网址,Nuxt.js使用了vue-meta更新应用头部Head和html属性
- 我们除了可以在index.html中手动设置标题和meta信息,还可以使用nuxt.config.js对head字段进行配置
js
head: {
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
],
link: [
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Roboto' }
]
}
- 如果是个别页面特定处理,可以参考个性化特定页面 head方法
html
<script>
export default {
data() {
return {
title: 'Hello World!'
}
},
head() {
return {
title: this.title,
meta: [
{
hid: 'description',
name: 'description',
content: 'My custom description'
}
]
}
}
}
</script>
- 所以在article/index.vue中添加head方法
js
// 加入这个对SEO非常有用
head () {
// 网页标题是文章标题 - 网站名
return {
title: `${this.article.title} - RealWorld`,
// hid:为了避免子组件中的meta标签不能正确覆盖父组件中相同的标签而产生重复的现象,利用hid为meta配一个唯一的标识编号。
meta: [{
hid: 'description',
name: 'description',
content: this.article.description
}]
}
}
通过客户端渲染展示评论列表
1.创建article/compponents/article-comments.vue
文件,然后将index.vue中的评论内容copy过去
html
<template>
<div>
<!-- 发表评论 -->
<form class="card comment-form">
...
</form>
<!-- 评论卡片 -->
<div class="card">
...
</div>
<div class="card">
...
</div>
</div>
</template>
<script>
export default {
name:'articleComments'
}
</script>
<style>
</style>
- 在index.vue中注册并引用组件,并把文章信息传过去
html
<article-comments :article="article"/>
js
import ArticleComments from './components/article-comments';
...
components: {
ArticleMeta,
ArticleComments
}
...
- 网页展示没有问题,那么在
api/article.js
中开始写获取文章评论的接口
js
// 获取文章评论
export const getComments = slug => {
return request ({
method: 'GET',
url: `/api/articles/${slug}/comments`
})
}
- 然后在组件comments中引用,因为这里不需要优化seo,所以使用客户端渲染页面
html
<script>
import { getComments } from '@/api/article'
export default {
name:'articleComments',
data () {
return {
comments: [] // 文章列表
}
},
props: {
article: {
type: Object,
required: true
}
},
async mounted () {
const { data } = await getComments(this.article.slug)
console.log(data)
this.comments = data.comments
}
}
</script>
- 可以看到打印出来的数据格式
js
{
"comments": [
{
"id": 81967,
"createdAt": "2021-01-07T10:02:17.803Z",
"updatedAt": "2021-01-07T10:02:17.803Z",
"body": "11",
"author": {
"username": "luopengppxcll",
"bio": "ceshiyixia",
"image": "https://st.quantrimang.com/photos/image/2016/11/11/anh-gif-3.gif",
"following": false
}
}
]
}
- 下面对页面进行修正
html
<!-- 发表评论 -->
<form class="card comment-form">
<div class="card-block">
<textarea class="form-control" placeholder="Write a comment..." rows="3"></textarea>
</div>
<div class="card-footer">
<!--这里使用的是用户头像-->
<img :src="article.author.image" class="comment-author-img" />
<button class="btn btn-sm btn-primary">
Post Comment
</button>
</div>
</form>
html
<!-- 评论卡片,对comments进行遍历 -->
<div class="card"
v-for="comment in comments"
:key="comment.id">
<div class="card-block">
<!--评论主体-->
<p class="card-text">{{ comment.body }}</p>
</div>
<div class="card-footer">
<!--用户头像和名称链接,跳转到用户的主页-->
<nuxt-link
class="comment-author"
:to="{
name: 'profile',
params: {
username: comment.author.username
}
}">
<img :src="comment.author.image" class="comment-author-img" />
</nuxt-link>
<nuxt-link
class="comment-author"
:to="{
name: 'profile',
params: {
username: comment.author.username
}
}">
{{ comment.author.username }}
</nuxt-link>
<!--显示评论的时间并添加过滤器-->
<span class="date-posted">{{ comment.createdAt | date('MMM DD, YYYY') }}</span>
</div>
</div>
- 评论列表也就完成了。
发布部署
发布部署看下面