1、介绍
案例名称:RealWorld
一个开源的学习项目,目的就是帮助开发者快速学习新技能
- GitHub仓库:https://github.com/gothinkster/realworld
- 在线示例:https://demo.realworld.io/#/
- 接口文档:https://github.com/gothinkster/realworld/tree/master/api
- 页面模板:https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md
2、项目初始化
1、创建项目
# 创建项目目录
mkdir realworld-nuxtjs
# 进入项目目录
cd realworld-nuxtjs
# 生成 package.json 文件
npm init -y
# 安装 nuxt 依赖
npm install nuxt
在 package.json 中添加启动脚本:
"scripts": {
"dev": "nuxt"
}
创建pages/index.vue:
<template>
<div>
<h1>Home Page</h1>
</div>
</template>
<script>
export default {
name: 'HomePage'
}
</script>
<style>
</style>
启动服务:npm run dev
在浏览器中访问http://localhost:3000/测试。
2、导入样式资源
app.html
https://www.jsdelivr.com CDN国内节点下载快
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
<head {{ HEAD_ATTRS }}>
{{ HEAD }}
<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on -->
<!-- //code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/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="/index.css">
</head>
<body {{ BODY_ATTRS }}>
{{ APP }}
</body>
</html>
3、布局组件
pages/layout/index.vue
<template>
<div>
<!-- 顶部导航栏 -->
<nav class="navbar navbar-light">
<div class="container">
<a class="navbar-brand" href="index.html">conduit</a>
<ul class="nav navbar-nav pull-xs-right">
<li class="nav-item">
<!-- Add "active" class when you're on that page" -->
<a class="nav-link active" href="">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="">
<i class="ion-compose"></i> New Post
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="">
<i class="ion-gear-a"></i> Settings
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="">Sign up</a>
</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>
<script>
export default {
name: 'LayoutIndex'
}
</script>
<style>
</style>
自己配置路由规则
Nuxt.js 配置文件
// nuxt.config.js
/**
* Nuxt.js 配置文件
*/
module.exports = {
router: {
linkActiveClass: 'active',
// 自定义路由表规则
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
<template>
<div class="home-page">
<div class="banner">
<div class="container">
<h1 class="logo-font">conduit</h1>
<p>A place to share your knowledge.</p>
</div>
</div>
<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">
<a class="nav-link disabled" href="">Your Feed</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="">Global Feed</a>
</li>
</ul>
</div>
<div class="article-preview">
<div class="article-meta">
<a href="profile.html"><img src="http://i.imgur.com/Qr71crq.jpg" /></a>
<div class="info">
<a href="" class="author">Eric Simons</a>
<span class="date">January 20th</span>
</div>
<button class="btn btn-outline-primary btn-sm pull-xs-right">
<i class="ion-heart"></i> 29
</button>
</div>
<a href="" class="preview-link">
<h1>How to build webapps that scale</h1>
<p>This is the description for the post.</p>
<span>Read more...</span>
</a>
</div>
<div class="article-preview">
<div class="article-meta">
<a href="profile.html"><img src="http://i.imgur.com/N4VcUeJ.jpg" /></a>
<div class="info">
<a href="" class="author">Albert Pai</a>
<span class="date">January 20th</span>
</div>
<button class="btn btn-outline-primary btn-sm pull-xs-right">
<i class="ion-heart"></i> 32
</button>
</div>
<a href="" class="preview-link">
<h1>The song you won't ever stop singing. No matter how hard you try.</h1>
<p>This is the description for the post.</p>
<span>Read more...</span>
</a>
</div>
</div>
<div class="col-md-3">
<div class="sidebar">
<p>Popular Tags</p>
<div class="tag-list">
<a href="" class="tag-pill tag-default">programming</a>
<a href="" class="tag-pill tag-default">javascript</a>
<a href="" class="tag-pill tag-default">emberjs</a>
<a href="" class="tag-pill tag-default">angularjs</a>
<a href="" class="tag-pill tag-default">react</a>
<a href="" class="tag-pill tag-default">mean</a>
<a href="" class="tag-pill tag-default">node</a>
<a href="" class="tag-pill tag-default">rails</a>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HomeIndex'
}
</script>
<style>
</style>
3、导入登录注册页面
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">
<!-- <a href="">Have an account?</a> -->
<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: 'LoginIndex',
computed: {
isLogin () {
return this.$route.name === 'login'
}
}
}
</script>
nuxt.config.js新增路由配置
{
path: '/login',
name: 'login',
component: resolve(__dirname, 'pages/login/')
},
{
path: '/register',
name: 'register',
component: resolve(__dirname, 'pages/login/')
},
4、导入剩余页面
参考页面模板:https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md 配置即可
Profile
pages/profile/index.vue
Settings
pages/settings/index.vue
Create/Edit Article
pages/editor/index.vue
Article
pages/article/index.vue
nuxt.config.js
{
path: '/profile/:username',
name: 'profile',
component: resolve(__dirname, 'pages/profile/')
},
{
path: '/settings',
name: 'settings',
component: resolve(__dirname, 'pages/settings/')
},
{
path: '/editor',
name: 'editor',
component: resolve(__dirname, 'pages/editor/')
},
{
path: '/article/:slug', // slug 文章链接名称
name: 'article',
component: resolve(__dirname, 'pages/article/')
}
5、处理顶部导航链接
a标签替换
/pages/layout/index.vue
<template>
<div>
<!-- 顶部导航栏 -->
<nav class="navbar navbar-light">
<div class="container">
<!-- <a class="navbar-brand" href="index.html">conduit</a> -->
<nuxt-link
class="navbar-brand"
to="/"
>Home</nuxt-link>
<ul class="nav navbar-nav pull-xs-right">
<li class="nav-item">
<!-- Add "active" class when you're on that page" -->
<!-- <a class="nav-link active" href="">Home</a> -->
<nuxt-link
class="nav-link"
to="/"
exact
>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/123">
<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>
<!-- /顶部导航栏 -->
<!-- 子路由 -->
<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>
<script>
import { mapState } from 'vuex'
export default {
name: 'LayoutIndex',
computed: {
...mapState(['user'])
}
}
</script>
<style>
</style>
6、处理顶部导航链接高亮
Nuxt.js 配置文件
router: {
linkActiveClass: 'active',
extendRoutes (routes, resolve) {
...
}
}
同时/pages/layout/index.vue去掉home的active
<nuxt-link
class="nav-link"
to="/"
exact // 精确匹配 路径必须==/ 才会高亮。不然会一直高亮因为都会以/开头。vue-router也有这个功能
>Home</nuxt-link>
7、封装请求模块
npm i axios
utils/request.js
import axios from'axios'
const request = axios.create({
baseURL: 'https://conduit.productionready.io/'
})
export default request
3、登录注册
1、实现基本登录功能
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">
<!-- <a href="">Have an account?</a> -->
<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 v-model="user.email" class="form-control form-control-lg" type="text" placeholder="Email">
</fieldset>
<fieldset class="form-group">
<input v-model="user.password" 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>
import request from '@/utils/req'
export default {
name: 'LoginIndex',
computed: {
isLogin () {
return this.$route.name === 'login'
}
},
data () {
return {
user: {
email: '',
password: ''
},
errors: {} // 错误信息
}
},
methods: {
async onSubmit () {
// 提交表单请求登录
const {data} = await request({
methods: 'POST',
url: '/api/users/login',
data: {user: this.user}
})
this.$router.push('/')
}
}
}
</script>
2、封装请求方法
api/user.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
})
}
pages/login/index.vue
// import request from '@/utils/req'
import { login, register } from '@/api/user'
methods: {
async onSubmit () {
// 提交表单请求登录
const {data} = await login({
user: this.user
})
this.$router.push('/')
}
}
3、表单验证
input添加required属性
格式 type修改
<form @submit.prevent="onSubmit">
<fieldset v-if="!isLogin" class="form-group">
<input v-model="user.username" class="form-control form-control-lg" type="text" placeholder="Your Name" required>
</fieldset>
<fieldset class="form-group">
<input v-model="user.email" class="form-control form-control-lg" type="email" placeholder="Email" required>
</fieldset>
<fieldset class="form-group">
<input v-model="user.password" class="form-control form-control-lg" type="password" placeholder="Password" required minlength="8">
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right">
{{ isLogin ? 'Sign in' : 'Sign up' }}
</button>
</form>
5、错误处理、用户注册
<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>
<fieldset v-if="!isLogin" class="form-group">
<input v-model="user.username" class="form-control form-control-lg" type="text" placeholder="Your Name" required>
</fieldset>
<script>
import { login, register } from '@/api/user'
// 仅在客户端加载 js-cookie 包
const Cookie = process.client ? require('js-cookie') : undefined
export default {
middleware: 'notAuthenticated',
name: 'LoginIndex',
computed: {
isLogin () {
return this.$route.name === 'login'
}
},
data () {
return {
user: {
username: '',
email: '[email protected]',
password: '12345678'
},
errors: {} // 错误信息
}
},
methods: {
async onSubmit () {
try {
// 提交表单请求登录
const { data } = this.isLogin
? await login({
user: this.user
})
: await register({
user: this.user
})
// console.log(data)
// TODO: 保存用户的登录状态
this.$store.commit('setUser', data.user)
// 为了防止刷新页面数据丢失,我们需要把数据持久化
Cookie.set('user', data.user)
// 跳转到首页
this.$router.push('/')
} catch (err) {
// console.log('请求失败', err)
this.errors = err.response.data.errors
}
}
}
}
</script>
6、解析存储登录状态实现流程
(1)初始化容器数据
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default () => {
return new Vuex.Store({
state: {user: null },
mutations: {
setUser (state, user) {state.user=user }
},
actions: {}
})
}
(2)登录成功,将用户信息存入容器
this.$store.commit('setUser', data.user)
(3)将登录状态持久化到 Cookie 中
npm i js-cookie
const Cookie = process.client ? require('js-cookie') : undefined
Cookie.set('user', data.user)
(4)从 Cookie 中获取并初始化用户登录状态
安装 cookieparser
npm i cookieparser
store/index.js
actions: {
// 初始化容器以及需要传递给客户端的数据
// 这个特性的action只会在服务端渲染期间运行
nuxtServerInit ({ commit }, { req }) {
let user = null
if (req.headers.cookie) {
// 将请求头中的 Cookie 字符串解析为一个对象
const parsed = cookieparser.parse(req.headers.cookie)
try {
// 将 user 还原为 JavaScript 对象
user=JSON.parse(parsed.user)
} catch (err) {
// No valid cookie found
}
}
commit('setUser', user)
}
}
7、将登录状态存储到容器中
见上述(3)
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)
}
}
8、处理导航栏链接展示状态
pages/layout/index.vue
<template>
<div>
<!-- 顶部导航栏 -->
<nav class="navbar navbar-light">
<div class="container">
<!-- <a class="navbar-brand" href="index.html">conduit</a> -->
<nuxt-link
class="navbar-brand"
to="/"
>Home</nuxt-link>
<ul class="nav navbar-nav pull-xs-right">
<li class="nav-item">
<!-- Add "active" class when you're on that page" -->
<!-- <a class="nav-link active" href="">Home</a> -->
<nuxt-link
class="nav-link"
to="/"
exact
>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/123">
<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>
<!-- /顶部导航栏 -->
<!-- 子路由 -->
<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>
<script>
import { mapState } from 'vuex'
export default {
name: 'LayoutIndex',
computed: {
...mapState(['user'])
}
}
</script>
<style>
</style>
9、处理页面访问权限
https://zh.nuxtjs.org/guide/routing/
middleware/auth.js
/**
* 验证是否登录的中间件
*/
export default function ({ store, redirect }) {
// If the user is not authenticated
if (!store.state.user) {
return redirect('/login')
}
}
middlewares/not-auth.js
export default function ({ store, redirect }) {
// If the user is authenticated redirect to home page
// 已登录状态不允许访问
if (store.state.user) {
return redirect('/')
}
}
在需要判断登录权限的页面中配置使用中间件。
export default {
...
// 在路由匹配组件渲染之前会先执行中间件处理
middleware: ['auth']
}
项目中
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/login/index.vue
export default {
middleware: 'notAuthenticated',
}
其他的引入
export default {
middleware: 'authenticated',
}
4、首页
1、展示公共文章列表
封装请求方法:
api/article.js
import { request } from '@/plugins/request'
// 获取公共文章列表
export const getArticles = params => {
return request({
method: 'GET',
url: '/api/articles',
params
})
}
获取数据:
async asyncData ({ params }) {
const { data } = await getArticle(params.slug)
return data
},
模板绑定:
<div class="article-preview" v-for="article in articles" :key="article.slug">
<div class="article-meta">
<nuxt-link class="author" :to="` /profile/${article.author.username}`">
<img:src="article.author.image" />
</nuxt-link>
<div class="info">
<nuxt-link class="author" :to="` /profile/${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 }">
<iclass="ion-heart"></i> {{ article.favoritesCount }}
</button>
</div>
<nuxt-link:to="{name: 'article' ,params: {slug: article.slug}}"class="preview-link">
<h1>{{ article.title }}</h1>
<p>{{ article.description }}</p>
<span@click="$router.push({name: 'article' ,params: {slug: article.slug}})">Read more...</span></nuxt-link>
</div>
2、分页处理
1、处理分页参数
async asyncData ({ query }) {
const page = Number.parseInt(query.page || 1)
const limit = 20
const { data } = await getArticles({
limit, // 每页大小
offset: (page - 1) * limit // 偏移量 跳过多少条
})
return {
limit,
page,
articlesCount: data.articlesCount,
articles: data.articles
}
}
2、页码处理
分页模版
<nav>
<ul class="pagination">
<li class="page-item active">
<nuxt-link class="page-link" to=" /">1</nuxt-link>
</li>
<li class="page-item">
<nuxt-link class="page-link" to=" /">2</nuxt-link>
</li>
<liclass="page-item">
<nuxt-link class="page-link" to=" /">3</nuxt-link>
</li>
</ul>
</nav>
1、使用计算属性计算总页码
computed:{
totalPage () {
return Math.ceil(this.articlesCount / this.limit)
}
}
2、遍历生成页码列表
<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>
3、设置导航链接
4、响应 query 参数的变化
watchQuery: ['page'], // 监听参数字符串的更改 热更新可能有问题 更改后需要刷新下整个问题
3、首页
1、展示文章标签列表
api/tag.js
import { request } from '@/plugins/request'
// 获取文章标签列表
export const getTags = () => {
return request({
method: 'GET',
url: '/api/tags'
})
}
// 获取公共文章列表
export const getYourFeedArticles = params => {
return request({
method: 'GET',
url: '/api/articles/feed',
params,
// headers: {
// // 添加用户身份,数据格式:Token空格Token数据
// Authorization: `Token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6NDgxMTYsInVzZXJuYW1lIjoibHB6OTk5IiwiZXhwIjoxNTk3NzQxNTA4fQ.2yO8Fss4hYnvsIN2UYHsutQ1hmYqSSAA-UrIRnP4DOY`
// }
})
}
async asyncData ({ query }) {
const page = Number.parseInt(query.page|| 1)
const limit = 20
const tab = query.tab || 'global_feed' // 默认高亮
const tag = query.tag
const loadArticles = tab === 'global_feed'
? getArticles
: getYourFeedArticles
// 优化并行异步任务
const [ articleRes, tagRes ] = await Promise.all([
loadArticles({
limit,
offset: (page - 1) * limit,
tag // 过滤标签内容
}),
getTags()
])
const { articles, articlesCount } = articleRes.data
const { tags } = tagRes.data
articles.forEach(article => article.favoriteDisabled = false)
return {
articles, // 文章列表
articlesCount, // 文章总数
tags, // 标签列表
limit, // 每页大小
page, // 页码
tab, // 选项卡
tag // 数据标签
}
},
处理标签列表链接和数据
<div class="col-md-3">
<div class="sidebar">
<p>Popular Tags</p>
<div class="tag-list">
<nuxt-link
:to="{
name: 'home',
query: {
tab: 'tag',
tag: item
}
}"
class="tag-pill tag-default"
v-for="item in tags"
:key="item"
>{{ item }}</nuxt-link>
</div>
</div>
</div>
watchQuery: ['page', 'tag'], // 触发asyncData方法执行
分页列表添加tag
<!-- 分页列表 -->
<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,
tag: $route.query.tag,
tab: tab
}
}"
>{{ item }}</nuxt-link>
</li>
</ul>
</nav>
<!-- /分页列表 -->
处理导航栏
// 表示登录
<li v-if="user" class="nav-item">
<nuxt-link
class="nav-link"
:class="{
active: tab === 'your_feed'
}"
exact
:to="{
name: 'home',
query: {
tab: 'your_feed'
}
}"
>Your Feed</nuxt-link>
</li>
<li class="nav-item">
// a链接全部替换为nuxt-link
<nuxt-link
class="nav-link"
:class="{
active: tab === 'global_feed' // 高亮
}"
exact // 防止都高亮
:to="{
name: 'home'
}"
>Global Feed</nuxt-link>
</li>
<li v-if="tag" class="nav-item">
<nuxt-link
class="nav-link"
:class="{
active: tab === 'tag'
}"
exact
:to="{
name: 'home',
query: {
tab: 'tag',
tag: tag
}
}"
># {{ tag }}</nuxt-link>
</li>
computed: {
...mapState(['user']),
totalPage () {
return Math.ceil(this.articlesCount / this.limit)
}
},
watchQuery: ['page', 'tag', 'tab'], // 响应路由参数tab变化
统一设置用户Token
plugins/requst.js
/**
* 基于 axios 封装的请求模块
*/
import axios from 'axios'
// 创建请求对象
export const request = axios.create({
baseURL: 'http://realworld.api.fed.lagounews.com'
})
// 通过插件机制获取到上下文对象(query、params、req、res、app、store...)
// 插件导出函数必须作为 default 成员
// 解构context
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)
})
}
nuxt.config.js
// 注册插件
plugins: [
'~/plugins/request.js', // 根路径出发
'~/plugins/dayjs.js'
]
文章发布日期格式
dayjs
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>
文章点赞
api/article.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`
})
}
<button
class="btn btn-outline-primary btn-sm pull-xs-right"
:class="{
active: article.favorited
}"
@click="onFavorite(article)"
:disabled="article.favoriteDisabled"
>
async onFavorite (article) {
article.favoriteDisabled = true
if (article.favorited) {
// 取消点赞
await deleteFavorite(article.slug)
article.favorited = false
article.favoritesCount += -1
} else {
// 添加点赞
await addFavorite(article.slug)
article.favorited = true
article.favoritesCount += 1
}
article.favoriteDisabled = false
}
3、TODO 加载中 loading
<div class="article-preview">
Loading articles...
</div>
4、TODO 无内容提示
<div v-if="!articles.length" class="article-preview">
No articles are here... yet.
</div>
5、展示关注文章列表
3、视图处理
- 没有登录不展示 my-feed
- 处理 tab 切换以及高亮问题
数据处理
封装接口方法:
处理数据获取:
结合分页标签:
统一添加数据 token
统一处理 401
展示文章标签列表
(1)封装接口请求方法
(2)数据过滤- 标签链接
- 分页页码链接
日期格式处理
文章点赞
5、文章详情
1、展示基本信息
api/article.js
// 获取文章详情
export const getArticle = slug => {
return request({
method: 'GET',
url: `/api/articles/${slug}`
})
}
// 获取文章评论
export const getComments = slug => {
return request({
method: 'GET',
url: `/api/articles/${slug}/comments`
})
}
async asyncData ({ params }) {
const { data } = await getArticle(params.slug)
const { article } = data
const md = new MarkdownIt()
article.body = md.render(article.body) // v-html="article.body"
return {
article
}
},
2、把Markdown转为HTML
npm install markdown-it --save
3、展示文章作者信息
组件
pages/article/components/article-meta.vue
<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 class="author" :to="{
name: 'profile',
params: {
username: article.author.username
}
}">
{{ 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 <span class="counter">(10)</span>
</button>
<button
class="btn btn-sm btn-outline-primary"
:class="{
active: article.favorited
}"
>
<i class="ion-heart"></i>
Favorite Post <span class="counter">(29)</span>
</button>
</div>
</template>
<script>
export default {
name: 'ArticleMeta',
props: {
article: {
type: Object,
required: true
}
}
}
</script>
<style>
</style>
4、设置页面meta优化SEO
head () {
return {
title: `${this.article.title} - RealWorld`,
meta: [
{ hid: 'description', name: 'description', content: this.article.description }
]
}
}
6、文章评论
pages/article/components/article-comments.vue
文章详情
发布文章
用户中心
个人中心
<template>
<div>
<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="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" />
<button class="btn btn-sm btn-primary">
Post Comment
</button>
</div>
</form>
<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>
</div>
</template>
<script>
import { getComments } from '@/api/article'
export default {
name: 'ArticleComments',
props: {
article: {
type: Object,
required: true
}
},
data () {
return {
comments: [] // 文章列表
}
},
async mounted () {
const { data } = await getComments(this.article.slug)
this.comments = data.comments
}
}
</script>
<style>
</style>
7、发布部署
Nuxt.js 提供了一系列常用的命令, 用于开发或发布部署。‘
命令 | 描述 |
---|---|
nuxt | 启动一个热加载的Web服务器(开发模式)localhost:3000。 |
nuxt build | 利用webpack编译应用,压缩JS和CSS资源(发布用)。 |
nuxt start | 以生产模式启动一个Web服务器 (需要先执行nuxt build)。 |
nuxt generate | 编译应用,并依据路由配置生成对应的HTML文件 (用于静态站点的部署)。 |
如果使用了 Koa/Express 等 Node.js Web 开发框架,并使用了 Nuxt 作为中间件,可以自定义 Web 服务器的启动入口:
命令 | 描述 |
---|---|
NODE_ENV=development nodemonserver/index.js | 启动一个热加载的自定义 Web 服务器(开发模式)。 |
NODE_ENV=production nodeserver/index.js | 以生产模式启动一个自定义 Web 服务器 (需要先执行nuxt build)。 |
参数
您可以使用 --help 命令来获取详细用法。常见的命令有:
- –config-file或-c:指定nuxt.config.js的文件路径。
- –spa或-s:禁用服务器端渲染,使用SPA模式
- –unix-socket或-n:指定UNIX Socket的路径。
你可以将这些命令添加至package.json:
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate"
}
这样你可以通过npm run 来执行相应的命令。如: npm run dev。
提示:要将参数传递给npm命令,您需要一个额外的–脚本名称(例如:npm run dev --参数–spa)
开发模式
可通过以下命令以开发模式启动带热加载特性的 Nuxt 服务:
nuxt
// 或
npm run dev
发布部署
最简单的部署方式
配置Host+port
压缩发布包
把发布包传到服务端 (ftp、git等)linux下 scp命令
解压
安装依赖
启动服务
nuxt.config.js
server: {
// 默认localhost只能用于本机访问
host: '0.0.0.0', //对外提供访问 会监听所有的网卡地址,外网地址就可以访问到。如果是本地那就是局域网
port: 3000
},
以下是需要传到服务端的文件
传之前·需要先连接服务端
ssh [email protected]
ls
mkdir realworld-nuxtjs
cd realword-nuxtjs
pwd // /root/realworld-nuxtjs
exit 退出服务端
scp .\realworld-nuxtjs.zip [email protected]:/root/realworld-nuxtjs
scp命令用来本地和服务端传输文件的一个工具’
重新连接web服务器
ssh [email protected]
cd realword-nuxtjs/
ls // \realworld-nuxtjs.zip
unzip realworld-nuxtjs.zip 解压
ls
ls -a 可以查看隐藏目录
npm install
npm run start 查看到服务服务端的局域网地址
但访问时要用公网ip+端口号访问
这种方式要占用命令行
使用PM2启动Node服务
用来专门管理nodejs进程的一个应用,通过它可以把node相关的应该运行在后台,保持运行状态。
GitHub仓库地址:https://github.com/Unitech/pm2
官方文档: https://pm2.io/
安装:npm install --global pm2
启动: pm2 start 脚本路径
在web服务端也就是生成环境服务器 这里安装工具
然后 pm2 start npm – start 相当于给npm传参 还是通过npm启动这个服务,但是这个服务被pm2进行管理的
此时可以访问
如果想关闭 pm2 stop id/name
pm2 stop 6 此时就访问不到了
命令 | 说明 |
---|---|
pm2 list | 查看应用列表 |
pm2 start | 启动应用 |
pm2 stop | 停止应用 |
pm2 reload | 重载应用 |
pm2 restart | 重启应用 |
pm2 delete | 删除应用 |
自动化部署
自动化部署介绍
传统的部署方式
更新:本地构建–》发布
更新:本地构建–》发布
现代化的部署方式
ci/cd持续集成服务:github的action gitlab的gitllab ci gitee的jekins等
准备自动部署内容
使用 GitHub Actions实现自动部署
CI/CD服务:
- jenkins
- Gitlab CI
- GitHub Actions
- Travis CI
- Circle CI
- …
环境准备 - Linux服务器
- 把代码提交到GitHub远程仓库
配置GitHub Access Token - 生成:https://github.com/settings/tokens 通过身份令牌来访问操作github仓库
- 配置到项目的Secrets中:https://github.com/lipengzhou/realworld-nuxtjs/settings/secrets
登录github settings–》Developer settings–>personal access tokens–>点击generate new token–>起名字Note(TOKEN)–>Select scopes只勾选第一个repo(对仓库完全的操作权限)–》点击 Generate token 生成的token只会显示一次,忘记了的话可以重新再生成一个–>复制令牌存储到一个地方
来到本地代码所托管的远程仓库github里面–》Settings–>Secrets–>点击 New secret–>Name(TOKEN)这边需要和脚本中的名字保持一致–》Value(把刚刚复制的token放进去)–》点击 add secret
配置 GitHub Actions 执行脚本
- 在项目根目录创 .github/workflows目录
- 下载main.yml到workflows目录中
- https://gist.github.com/lipengzhou/b92f80142afa37aea397da47366bd872 打开后点击Raw 然后ctrl+s保存到上述目录中 保存类型所有文件(.) 文件名main.yml。 这里面就是些github actions所识别执行的脚本
- 修改配置
- 配置PM2配置文件
pm2.config.json
{
"apps":[
"name":"RealWorld",
"script":"npm",
"args":"start"
]
}
打开远程仓库 Secrets 添加USERNAME\TOKEN\PORT\PASSWORD\HOST
- 提交更新
- git add.
git tag v0.1.0
git tag
git push origin v0.1.0 把本地标签推送到远程服务
打开仓库 打开branch 打开Tags
看Actions 这边出发了发布部署测试 然后打开
看到build-and-deploy 然后打开 就会看到构建部署
- git add.
- 查看自动部署状态
- 访问网站
- 提交更新…
git add .
git commit -m ‘部署更新测试’
git push
git tag v0.1.1
git push origin v0.1.1
Nuxt.js 提供了两种发布部署应用的方式:服务端渲染应用部署和静态应用部署。
部署 Nuxt.js 服务端渲染的应用不能直接使用nuxt命令,而应该先进行编译构建,然后再启动 Nuxt服务,可通过以下两个命令来完成:
nuxt build
nuxt start
推荐的package.json配置如下:
{
"name": "my-app",
"dependencies": {
"nuxt": "latest"
},
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start"
}
}
提示:建议将.nuxt加入.npmignore和.gitignore文件中。
{
"apps": [
{
"name": "futurestudio-homepage",
"script": "npm",
"args": "start"
}
]
}
(1)结合 Express 服务器
const { Nuxt, Builder } = require('nuxt')
const app = require('express')()
const isProd = (process.env.NODE_ENV === 'production')
const port = process.env.PORT || 3000
// 用指定的配置对象实例化Nuxt.js
const config = require('./nuxt.config.js')
config.dev = !isProd
const nuxt = new Nuxt(config)
// 用 Nuxt.js 渲染每个路由
app.use(nuxt.render)
// 在开发模式下启用编译构建和热加载
if (config.dev) {
new Builder(nuxt).build().then(listen)
} else {
listen()
}
function listen () {
// 服务端监听
app.listen(port, '0.0.0.0')
console.log('Server listening on `localhost:' + port + '`.')
}
(2)使用 pm2 运行服务器
pm2 init
module.exports = {
apps: [{
name: 'rw',
script: 'app.js',
env: { NODE_ENV: 'production' }
}]
}
(3)发布到服务器
github/workflows/main.yml