Bootstrap

NuxtJS综合案例

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>&nbsp;New Post
	            </a>
	          </li>
	          <li class="nav-item">
	            <a class="nav-link" href="">
	              <i class="ion-gear-a"></i>&nbsp;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 &amp; 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>&nbsp;New Post
              </nuxt-link>
            </li>
            <li class="nav-item">
              <nuxt-link
                class="nav-link"
                to="/settings"
              >
                <i class="ion-gear-a"></i>&nbsp;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 &amp; 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>&nbsp;New Post
              </nuxt-link>
            </li>
            <li class="nav-item">
              <nuxt-link
                class="nav-link"
                to="/settings"
              >
                <i class="ion-gear-a"></i>&nbsp;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 &amp; 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>
      &nbsp;
      Follow Eric Simons <span class="counter">(10)</span>
    </button>
    &nbsp;&nbsp;
    <button
      class="btn btn-sm btn-outline-primary"
      :class="{
        active: article.favorited
      }"
    >
      <i class="ion-heart"></i>
      &nbsp;
      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>
        &nbsp;
        <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 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

;