Nuxt综合案例实现
前言
我们要实现的Demo[小伙伴可以自行点进去看看哈~]
https://demo.realworld.io/#/
以下是当前Demo的相关资源列举
- GitHub 仓库
https://github.com/gothinkster/realworld
- 接口文档
https://github.com/gothinkster/realworld/tree/master/api
- 页面模板
https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md
准备工作
项目初始化
mkdir [projectname]
cd [projectname]
yarn init -y
yarn add nuxt
---
vi package.json
insert content
"scripts": {
"dev": "nuxt"
},
esc :wq
---
mkdir pages
touch index.vue
index.vue
<template>
<div class="home">
home
</div>
</template>
<script>
export default {
name: 'home',
};
</script>
<style lang="scss" scoped></style>
yarn dev
打开响应端口页面,看到页面展示内容就说明我们的项目初始化完成啦~
导入页面模板
导入样式资源
touch app.html
- 将App.html 默认模板 copy 到 app.html 中
- 将realworld资源 资源部分导入到文件中
- 重启服务,F12验证资源加载
资源本土化方法
open https://www.jsdelivr.com/
search ionicons
找到对应的版本[2.0.1]下的css/ionicons.min.css
copy [链接](https://cdn.jsdelivr.net/npm/ionicons@2.0.1/css/ionicons.min.css)
将href替换成我们新的资源地址
open //demo.productionready.io/main.css
copy 内容
新建 static/main.css
将内容粘贴进去
替换资源引入'/main.css'
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
<head {{ HEAD_ATTRS }}>
{{ HEAD }}
<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on -->
<link
href="https://cdn.jsdelivr.net/npm/ionicons@2.0.1/css/ionicons.min.css"
rel="stylesheet"
type="text/css"
/>
<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"
/>
<!-- Import the custom Bootstrap 4 theme from our hosted CDN -->
<!-- <link rel="stylesheet" href="//demo.productionready.io/main.css" /> -->
<link rel="stylesheet" href="/main.css" />
</head>
<body {{ BODY_ATTRS }}>
{{ APP }}
</body>
</html>
配置布局组件
-
创建文件
mkdir pages/layouttouch pages/layout/index.vue
-
将realworld资源 中Header/Footer 部分copy到文件中
-
touch nuxt.config.js
module.exports = { router: { // 自定义路由规则 extendRoutes(routes, resolve) { // 清除 Nuxt.js 基于 pages 目录默认生成的路由表规则 routes.splice(0); routes.push( ...[ { name: 'layout', path: '/', component: resolve(__dirname, 'pages/layout/'), }, ] ); }, }, };
-
创建主页面相关
mkdir pages/home touch pages/home/index.vue
将realworld资源 中Home 部分copy到文件中
配置路由
module.exports = { router: { // 自定义路由规则 extendRoutes(routes, resolve) { // 清除 Nuxt.js 基于 pages 目录默认生成的路由表规则 routes.splice(0); routes.push( ...[ { name: 'layout', path: '/', component: resolve(__dirname, 'pages/layout/'), children: [ { path: '', // 默认子路由 name: 'home', component: resolve(__dirname, 'pages/home/'), }, ], }, ] ); }, }, };
配置页面组件
导入登录注册页面
-
创建pages/login/index.vue
-
从资源 Login/Register 中将内容拷贝到文件中
-
相关路由配置
children: [ { path: '/login', name: 'login', component: resolve(__dirname, 'pages/login/') }, { path: '/register', name: 'register', component: resolve(__dirname, 'pages/login/') }, ],
-
处理页面pages/login/index.vue
<template> <div class="auth-page"> <div class="container page"> <div class="row"> <div class="col-md-6 offset-md-3 col-xs-12"> <h1 class="text-xs-center">{{ isLogin ? 'Sign in' : 'Sign up' }}</h1> <p class="text-xs-center"> <nuxt-link v-if="isLogin" to="/register" >Need an account?</nuxt-link > <nuxt-link v-else to="/login">Have an account?</nuxt-link> </p> <ul class="error-messages"> <li>That email is already taken</li> </ul> <form> <fieldset v-if="!isLogin" class="form-group"> <input class="form-control form-control-lg" type="text" placeholder="Your Name" /> </fieldset> <fieldset class="form-group"> <input class="form-control form-control-lg" type="text" placeholder="Email" /> </fieldset> <fieldset class="form-group"> <input class="form-control form-control-lg" type="password" placeholder="Password" /> </fieldset> <button class="btn btn-lg btn-primary pull-xs-right"> {{ isLogin ? 'Sign in' : 'Sign up' }} </button> </form> </div> </div> </div> </div> </template> <script> export default { name: 'Login', computed: { isLogin() { return this.$route.name === 'login'; }, }, }; </script> <style lang="scss" scoped></style>
导入剩余页面
按照规则配置页面的内容以及路由即可
- 创建pages/profile/index.vue
- 从资源 Login/Register 中将内容拷贝到文件中
- 相关路由配置
处理导航链接
1. 处理导航链接
这里主要将顶部导航的a链接替换成nuxt-link 处理到正确的路由
处理完成的页面pages/layout/index.vue
<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">
<!-- Add "active" class when you're on that page" -->
<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/1">
<img class="user-pic" src="" />
crystalangel
</nuxt-link>
</li>
</ul>
</div>
</nav>
<!-- /顶部导航栏 -->
<!-- 子路由 -->
<nuxt-child />
<!-- /子路由 -->
<!-- 底部 -->
<footer>
<div class="container">
<a href="/" class="logo-font">conduit</a>
<span class="attribution">
An interactive learning project from
<a href="https://thinkster.io">Thinkster</a>. Code & design
licensed under MIT.
</span>
</div>
</footer>
<!-- /底部 -->
</div>
</template>
2. 处理导航链接高亮
active className处理
nuxt.config.js
module.exports = {
router: {
linkActiveClass: 'active',
}
}
pages/layout/index.vue
修改home 的nuxt-link
<nuxt-link class="nav-link" exact to="/">Home</nuxt-link>
封装请求模块
1. 安装依赖 yarn add axios
2. 创建 utils/request.js 文件
import axios from 'axios';
// https://conduit.productionready.io
const request = axios.create({
baseUrl: 'https://conduit.productionready.io',
});
export default request;
登录注册功能实现
基本登录功能
- 绑定表单数据
- 注册点击登录事件
- 点击登录请求接口,登陆成功之后返回首页
由于事件是很基础的,我们这里直接记录当前功能完成之后的页面代码
pages/login/index.vue
<template>
<div class="auth-page">
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">{{ isLogin ? 'Sign in' : 'Sign up' }}</h1>
<p class="text-xs-center">
<nuxt-link v-if="isLogin" to="/register"
>Need an account?</nuxt-link
>
<nuxt-link v-else to="/login">Have an account?</nuxt-link>
</p>
<ul class="error-messages">
<li>That email is already taken</li>
</ul>
<form @submit.prevent="onSubmit">
<fieldset v-if="!isLogin" class="form-group">
<input
class="form-control form-control-lg"
type="text"
placeholder="Your Name"
/>
</fieldset>
<fieldset class="form-group">
<input
class="form-control form-control-lg"
type="text"
placeholder="Email"
v-model="user.email"
/>
</fieldset>
<fieldset class="form-group">
<input
class="form-control form-control-lg"
type="password"
placeholder="Password"
v-model="user.password"
/>
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right">
{{ isLogin ? 'Sign in' : 'Sign up' }}
</button>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
import request from '@/utils/request';
export default {
name: 'Login',
computed: {
isLogin() {
return this.$route.name === 'login';
},
},
data() {
return {
user: {
email: '',
password: '',
},
};
},
methods: {
async onSubmit() {
const { data } = await request({
method: 'POST',
url: '/api/users/login',
data: { user: this.user },
});
console.log(data);
if (data) this.$router.push('/');
},
},
};
</script>
封装请求方法
在项目里面的接口请求,为了维护的方便,我们会将接口封装到统一的目录下
- 新建
api/user.js
文件 - 封装接口
- 替换页面使用方法
api/user.js
import request from '@/utils/request';
export const login = (data) =>
request({
method: 'POST',
url: '/api/users/login',
data,
});
pages/login/index.vue
import { login } from '@/api/user';
export default {
methods: {
async onSubmit() {
const { data } = await login({ user: this.user });
console.log(data);
if (data) this.$router.push('/');
},
},
}
表单验证
我们这里专注于体验Nuxt 所以 验证的地方直接使用原生的input 验证
<fieldset v-if="!isLogin" class="form-group">
<input
class="form-control form-control-lg"
type="text"
placeholder="Your Name"
required
/>
</fieldset>
<fieldset class="form-group">
<input
class="form-control form-control-lg"
type="email"
placeholder="Email"
v-model="user.email"
required
/>
</fieldset>
<fieldset class="form-group">
<input
class="form-control form-control-lg"
type="password"
placeholder="Password"
v-model="user.password"
required
/>
</fieldset>
错误处理
我们当前的情况是:如果用户输入错误,页面会直接跳到 Request failed 页面
正常情况下我们应该在用户输入错误之后将错误信息显示到页面中,处理方法就是将我们提交方法try catch起来,然后捕获错误信息,将错误信息展示到页面中
<template>
...
<ul class="error-messages">
<template v-for="(messages, field) in errors">
<li v-for="(message, index) in messages" :key="index">
{{ field }} {{ message }}
</li>
</template>
</ul>
...
</template>
<script>
export default {
...
data() {
return {
...
errors: {}, // 错误信息
};
},
methods: {
async onSubmit() {
try {
const { data } = await login({ user: this.user });
console.log(data);
if (data) this.$router.push('/');
} catch (error) {
// console.dir(error);
this.errors = error.response.data.errors;
}
},
},
};
</script>
用户注册
用户注册功能整体逻辑和登录大同小异
- 绑定表单用户名信息
- 完善密码长度校验
- sign up 请求接口信息
- 请求成功跳转到首页
- 请求失败将失败信息展示到页面
pages/login/index.vue
<template>
...
<fieldset v-if="!isLogin" class="form-group">
<input
class="form-control form-control-lg"
type="text"
placeholder="Your Name"
required
v-model="user.username"
/>
</fieldset>
...
<fieldset class="form-group">
<input
class="form-control form-control-lg"
type="password"
placeholder="Password"
v-model="user.password"
minlength="8"
required
/>
</fieldset>
...
</template>
<script>
import { login, register } from '@/api/user';
export default {
...
data() {
return {
user: {
username: '',
email: '',
password: '',
},
errors: {}, // 错误信息
};
},
methods: {
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;
}
},
},
};
</script>
api/user.js
import request from '@/utils/request';
export const register = (data) =>
request({
method: 'POST',
url: '/api/users',
data,
});
存储登录状态
登录状态我们需要在多个页面内使用,所以这里我们需要一个容器将其存储起来,由于服务环境下依然需要这个状态,所以实现起来跟VueJS中有些不同
这里可以先看一下官方示例的实现,我们的实现过程和官方的其实是一致的
- 初始化容器: 创建
store/index.js
- 将登录状态存储到容器中
- 登录状态持久化处理(防止页面刷新之后状态丢失的问题)【将状态存放到cookie中,方便在服务器中依然可以拿到状态,需要安装依赖 js-cookie\cookieparser】
store/index.js
const cookieparser = process.server ? require('cookieparser') : undefined;
// 在服务端渲染期间运行都是同一个实例
// 为了防止数据冲突,务必要把 state 定义成一个函数,返回数据对象
export const state = () => {
return {
// 当前登录用户的登录状态
user: null,
};
};
export const mutations = {
setUser(state, data) {
state.user = data;
},
};
export const actions = {
// nuxtServerInit 是一个特殊的 action 方法
// 这个 action 会在服务端渲染期间自动调用
// 作用:初始化容器数据,传递数据给客户端使用
nuxtServerInit({ commit }, { req }) {
let user = null;
// 如果请求头中有 Cookie
if (req.headers.cookie) {
// 使用 cookieparser 把 cookie 字符串转为 JavaScript 对象
const parsed = cookieparser.parse(req.headers.cookie);
try {
user = JSON.parse(parsed.user);
} catch (err) {
// No valid cookie found
}
}
// 提交 mutation 修改 state 状态
commit('setUser', user);
},
};
pages/login/index.vue
methods: {
async onSubmit() {
try {
const { data } = this.isLogin
? await login({ user: this.user })
: await register({ user: this.user });
// 存储用户登录状态
this.$store.commit('setUser', data.user);
// 登录状态持久化
Cookie.set('user', data.user);
this.$router.push('/');
} catch (error) {
// console.dir(error);
this.errors = error.response.data.errors;
}
},
},
处理导航栏链接展示状态
根据用户登录状态处理导航栏链接展示状态
在用户未登录的状态下,导航栏应该展示sign in
sign up
用户登录的状态下,应该展示new post
settings
user info
pages/layout/index.vue
<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">
<!-- Add "active" class when you're on that page" -->
<nuxt-link class="nav-link" exact to="/">Home</nuxt-link>
</li>
<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/1">
<img class="user-pic" :src="user.image" />
{{ user.username }}
</nuxt-link>
</li>
</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>
</div>
</nav>
...
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'Layout',
computed: {
...mapState(['user']),
},
};
</script>
<style lang="scss" scoped></style>
处理页面访问权限
在用户没有登录的情况下,我们应该是不允许用户访问
Nuxt 为我们提供了 路由中间件 的处理方案,这里的处理包含了服务端and 客户端的处理
未登录状态下需要处理的页面有: editor, profile, settings
登录状态下需要处理的页面有: sign up, sign in
- 创建文件
middleware/authenticated.js
middleware/notAuthenticated.js
- 处理需要处理的文件:文件中添加相应的中间件即可
middleware/authenticated.js
export default function({ store, redirect }) {
// If the user is not authenticated
if (!store.state.user) {
return redirect('/login');
}
}
middleware/notAuthenticated.js
export default function({ store, redirect }) {
// If the user is authenticated redirect to home page
if (store.state.user) {
return redirect('/');
}
}
pages/editor/index.vue 【这里只列举一个使用,其他类似】
<script>
export default {
name: 'Editor',
middleware: 'authenticated',
};
</script>
首页
公共文章列表
1. 展示
- 封装数据接口
- 请求获取数据
- 绑定数据到视图
创建 api/article.js
import request from '@/utils/request';
// 获取公共文章列表
export const getArticles = (params) =>
request({
method: 'GET',
url: '/api/articles',
params,
});
pages/home/index.vue
<template>
<div class="home-page">
<div class="container page">
<div class="row">
<div class="col-md-9">
...
<div
class="article-preview"
v-for="article in articles"
:key="article.slug"
>
<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
class="author"
:to="{
name: 'profile',
params: {
username: article.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>
<nuxt-link
class="preview-link"
:to="{
name: 'article',
params: {
slug: article.slug,
},
}"
>
<h1>
{{ article.title }}
</h1>
<p>{{ article.description }}</p>
<span>Read more...</span>
</nuxt-link>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { getArticles } from '@/api/article';
export default {
name: 'Home',
async asyncData() {
const { data } = await getArticles();
return {
articles: data.articles,
articlesCount: data.articlesCount,
};
},
};
</script>
2. 分页处理
监听query 改变
- 分页参数处理
- 添加页码
- 将页码路由 query 处理
pages/home/index.vue
<template>
<div class="home-page">
...
<!-- 分页列表 -->
<nav>
<ul class="pagination">
<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,
},
}"
>{{ item }}</nuxt-link
>
</li>
</ul>
</nav>
<!-- /分页列表 -->
</div>
...
</div>
</div>
</div>
</template>
<script>
import { getArticles } from '@/api/article';
export default {
name: 'Home',
watchQuery: ['page'],
async asyncData({ query }) {
const page = Number.parseInt(query.page || 1);
const limit = 10;
const { data } = await getArticles({
limit,
offset: (page - 1) * limit,
});
return {
articles: data.articles,
articlesCount: data.articlesCount,
limit,
page,
};
},
computed: {
totalPage() {
return Math.ceil(this.articlesCount / this.limit);
},
},
};
</script>
文章标签列表
1. 展示
1. 封装列表接口
2. 请求接口数据
3. 绑定数据到视图
新建 api/tag.js
import request from '@/utils/request';
// 获取文章标签列表
export const getTags = () => {
return request({
method: 'GET',
url: '/api/tags',
});
};
pages/home/index.vue
<template>
<div class="home-page">
...
<div class="col-md-3">
<div class="sidebar">
<p>Popular Tags</p>
<div class="tag-list">
<nuxt-link
:to="{
name: 'home',
query: {
tag: tag,
},
}"
class="tag-pill tag-default"
v-for="tag in tags"
:key="tag"
>
{{ tag }}
</nuxt-link>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { getArticles } from '@/api/article';
import { getTags } from '@/api/tag';
export default {
name: 'Home',
watchQuery: ['page', 'tag'],
async asyncData({ query }) {
const page = Number.parseInt(query.page || 1);
const limit = 10;
const { data } = await getArticles({
limit,
offset: (page - 1) * limit,
tag: query.tag,
});
const { data: tagData } = await getTags();
return {
articles: data.articles,
articlesCount: data.articlesCount,
limit,
page,
tags: tagData.tags,
};
}
};
</script>
2. 优化并行异步任务
<script>
export default {
...
async asyncData({ query }) {
const page = Number.parseInt(query.page || 1);
const limit = 10;
const [articleRes, tagsRes] = await Promise.all([
getArticles({
limit,
offset: (page - 1) * limit,
tag: query.tag,
}),
getTags(),
]);
const { articles, articlesCount } = articleRes.data;
const { tags } = tagsRes.data;
return {
articles,
articlesCount,
limit,
page,
tags,
};
},
...
};
</script>
3. 对应文章标签和文章列表
- 点击对应的Tag标签,将相应的tag 映射到连接中
- 页面监听到query中tag 的改变,请求接口返回数据更新视图
- 处理tag 列表下的page 改动的请求
导航栏部分
1. 展示状态处理
- 关注的列表只能在登录状态下展示
- 标签列表只能再点击了tag 之后才展示
- 切换到关注的列表或者全部列表下不展示标签列表
- 标签高亮部分处理
- 导航部分处理:根据query 中的tab 来识别当前列表
pages/home/index.vue
<template>
<div class="home-page">
<div class="container page">
<div class="row">
<div class="col-md-9">
<div class="feed-toggle">
<ul class="nav nav-pills outline-active">
<li class="nav-item" v-if="user">
<nuxt-link
class="nav-link"
:class="{
active: tab === 'your_feed',
}"
:to="{
name: 'home',
query: {
tab: 'your_feed',
},
}"
exact
>Your Feed</nuxt-link
>
</li>
<li class="nav-item">
<nuxt-link
class="nav-link"
:class="{
active: tab === 'global_feed',
}"
:to="{
name: 'home',
query: {
tab: 'global_feed',
},
}"
exact
>Global Feed</nuxt-link
>
</li>
<li class="nav-item" v-if="tag">
<nuxt-link
class="nav-link"
:class="{
active: tab === 'tag',
}"
:to="{
name: 'home',
query: {
tag,
tab: 'tag',
},
}"
>#{{ tag }}</nuxt-link
>
</li>
</ul>
</div>
...
</div>
<div class="col-md-3">
<div class="sidebar">
<p>Popular Tags</p>
<div class="tag-list">
<nuxt-link
:to="{
name: 'home',
query: {
tag,
tab: 'tag',
},
}"
class="tag-pill tag-default"
v-for="tag in tags"
:key="tag"
>
{{ tag }}
</nuxt-link>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { getArticles } from '@/api/article';
import { getTags } from '@/api/tag';
import { mapState } from 'vuex';
export default {
name: 'Home',
watchQuery: ['page', 'tag', 'tab'],
async asyncData({ query }) {
const page = Number.parseInt(query.page || 1);
const limit = 10;
const [articleRes, tagsRes] = await Promise.all([
getArticles({
limit,
offset: (page - 1) * limit,
tag: query.tag,
}),
getTags(),
]);
const { articles, articlesCount } = articleRes.data;
const { tags } = tagsRes.data;
return {
articles,
articlesCount,
limit,
page,
tags,
tab: query.tab || 'global_feed',
tag: query.tag,
};
},
computed: {
...mapState(['user']),
totalPage() {
return Math.ceil(this.articlesCount / this.limit);
},
},
};
</script>
2. 对应数据与状态的对应关系
- 在切换页码的时候处理一下query 值
- 处理 关注的列表下的标签列表和全部列表下的标签列表
!
这里注意一下,关注的列表需要Token 才能正常访问,否则会爆出401错误,我们这里暂时写死token 处理,后续集中处理该问题
api/article.js
// 获取关注的文章列表
export const getFeedArticles = (params) =>
request({
method: 'GET',
url: '/api/articles/feed',
params,
headers: {
Authorization: `Token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTUzMzY2LCJ1c2VybmFtZSI6IuiNieiOkyIsImV4cCI6MTYyMjEwMDU3OH0.yUfoKBjRli1pf5WVWDHgTd19tJUDESYqgh6R_6RDDPs`,
},
});
pages/home/index.vue
<script>
import { getArticles, getFeedArticles } from '@/api/article';
import { getTags } from '@/api/tag';
import { mapState } from 'vuex';
export default {
name: 'Home',
watchQuery: ['page', 'tag', 'tab'],
async asyncData({ query }) {
const page = Number.parseInt(query.page || 1);
const limit = 2;
const { tab = 'global_feed', tag } = query;
const loadArticles = tab === 'global_feed' ? getArticles : getFeedArticles;
const [articleRes, tagsRes] = await Promise.all([
loadArticles({
limit,
offset: (page - 1) * limit,
tag,
}),
getTags(),
]);
const { articles, articlesCount } = articleRes.data;
const { tags } = tagsRes.data;
return {
articles,
articlesCount,
limit,
page,
tags,
tab,
tag,
};
}
};
</script>
设置用户Token
前置知识
由于我们需要在拦截器中访问当前用户的信息,所以我们需要使用Nuxt 插件进行处理这件事
- 遵循Nuxt标准,我们需要将util 中的封装,转移到plugins 中[修改util 文件夹名字为plugins]
- 在 nuxt.config.js 中注册插件
- 处理request.js 中的拦截器内容[见官方使用]
- 修改request调用处为当前调用
plugins/request.js
import axios from 'axios';
// https://conduit.productionready.io
export const request = axios.create({
baseURL: 'https://conduit.productionready.io',
});
// 通过插件机制获取到上下文对象(query、params、req、res、app、store...)
// 插件导出函数必须作为 default 成员
export default ({ store }) => {
// 请求拦截器
// Add a request interceptor
// 任何请求都要经过请求拦截器
// 我们可以在请求拦截器中做一些公共的业务处理,例如统一设置 token
request.interceptors.request.use(
function(config) {
// Do something before request is sent
// 请求就会经过这里
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);
}
);
};
发布时间格式化
我们这里使用一个插件处理
考虑到我们也会在文章中使用格式化处理时间,所以我们这里同样注册一个插件
-
新建文件
plugins/dayjs.js
import Vue from 'vue'; import dayjs from 'dayjs'; Vue.filter('date', (value, format = 'YYYY-MM-DD HH:mm:ss') => { return dayjs(value).format(format); });
-
注册插件
-
使用插件
<span class="date"> {{ article.createdAt | date('MMM DD, YYYY') }} </span>
文章点赞
功能描述:
点击favoriteIcon 可添加点赞,数量加一,favorite点亮,再次点击,取消点赞
methods: {
async onFavorite(article) {
article.favoriteDisabled = true;
if (article.favorited) {
// 取消赞
await onCancelFavorite(article.slug);
article.favorited = false;
article.favoritesCount -= 1;
} else {
// 赞
await onFavorite(article.slug);
article.favorited = true;
article.favoritesCount += 1;
}
article.favoriteDisabled = false;
},
},
文章详情页
1. 数据动态展示
- 接口封装
- 将请求数据绑定到页面视图
- 解析文章内容markdown为html 格式【使用markdown-it插件】
- 封装文章作者信息使用
2. 设置页面meta
3. 文章评论
可以提交、删除评论
- 将评论部分抽象为组件展示
- 封装接口
- 将数据绑定到视图展示
发布部署
- 打包 nuxt应用
1. 添加相应的指令到package.json 2. 执行打包命令 3. 使用start 验证打包结果
自动部署
使用GitHub Actions 实现自动部署
1. CI/CD 服务
目的: 实现持续集成/部署
- Jenkins
- Gitlab CI
GitHub Actions
- Travis CI
- Circle CI
- …
2. 通过 GitHub Actions 实现自动部署
环境准备
- linux 服务器
- 把代码提交到GitHub 远程仓库
配置GitHub Access Token
作用:在CI/CD 中使用Github用户的身份令牌来访问/操作Github仓库
- 生成 https://github.com/settings/tokens
- 配置到项目的Secrets 中 https://github.com/[your github name]/[your github project]/settings/secrets/actions
配置 GitHub Actions 执行脚本
- 在项目根目录创建 .github/workflows 目录
- 下载 main.yml 到 workflow 目录中
- 修改配置
- 配置PM2 配置文件(根目录下创建pm2.config.json)
- 提交更新
- 查看自动部署状态
- 访问网站
- 提交更新…
下载main.yml 之后修改的部分
创建secrets 需要的 secrets.HOST…
推送代码到远程仓库
项目打版 git tag v***