IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> Nuxt-js-综合案例(27) -> 正文阅读

[JavaScript知识库]Nuxt-js-综合案例(27)

Nuxt.js 综合案例

介绍

  • 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

创建项目

# 创建项目目录
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>

导入样式资源

当前目录结构:

1627261549024

app.html

<!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="/index.css">
</head>
<body {{ BODY_ATTRS }}>
{{ APP }}
</body>
</html>

配置布局组件

pages/layout/index.vue

<template>
  <div>
    <!--  header -->
    <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>
    <!--  header -->

    <!--    子路由  -->
    <nuxt-child/>

    <!--    footer  -->
    <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>
    <!--    footer  -->
  </div>
</template>

<script>
export default {
  name: "LayoutIndex"
}
</script>

<style scoped>

</style>

添加nuxt.config.js配置文件,配置自定义路由表

/*
* Nuxt.js配置文件
* */

module.exports = {
  router: {
    // 自定义路由表规则
    extendRoutes(routes, resolve) {
      // 首先清空数组,清空nuxt默认生成的路由表
      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 scoped>

</style>

当前效果:

访问localhost:3000时,首先加载pages/index.vue组件,在nuxt-child中加载子路由,子路由path为空字符串‘’,因此访问localhost:3000时会同时加载pages/index.vuepages/layout/index.vuepages/home/index.vue三个组件

登录注册

在模板网址中找到登录模板,由于当前项目登录/注册业务不复杂,所以使用同一个组件模板,利用this.$route.name将其处理为动态组件。其中需要处理文字显示、按钮显示、路有指向,代码如下:

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: "LoginIndex",
  computed: {
    isLogin() {
      return this.$route.name === 'login'
    }
  }
}
</script>

<style scoped>

</style>
  • 动态部分:

    动态控制

    computed: {
      isLogin() {
        return this.$route.name === 'login'
      }
    }
    
    • 文字显示

      <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>
      
    • 输入框

      <fieldset v-if="!isLogin" class="form-group">
        <input class="form-control form-control-lg" type="text" placeholder="Your Name">
      </fieldset>
      
    • 按钮

      <button class="btn btn-lg btn-primary pull-xs-right">
        {{ isLogin ? 'Sign in' : 'Sign up' }}
      </button>
      

导入剩余页面

路径页面
/首页
/login登录
/register注册
/settings用户设置
/editor发布文章
/editor/:slug编辑文章
/profile/:username文章详情
/profile/:username/favorites用户页面/喜欢的文章

用户页面

pages/profile/index.vue

<template>
  <div class="profile-page">

    <div class="user-info">
      <div class="container">
        <div class="row">

          <div class="col-xs-12 col-md-10 offset-md-1">
            <img src="http://i.imgur.com/Qr71crq.jpg" class="user-img"/>
            <h4>Eric Simons</h4>
            <p>
              Cofounder @GoThinkster, lived in Aol's HQ for a few months, kinda looks like Peeta from the Hunger Games
            </p>
            <button class="btn btn-sm btn-outline-secondary action-btn">
              <i class="ion-plus-round"></i>
              &nbsp;
              Follow Eric Simons
            </button>
          </div>

        </div>
      </div>
    </div>

    <div class="container">
      <div class="row">

        <div class="col-xs-12 col-md-10 offset-md-1">
          <div class="articles-toggle">
            <ul class="nav nav-pills outline-active">
              <li class="nav-item">
                <a class="nav-link active" href="">My Articles</a>
              </li>
              <li class="nav-item">
                <a class="nav-link" href="">Favorited Articles</a>
              </li>
            </ul>
          </div>

          <div class="article-preview">
            <div class="article-meta">
              <a href=""><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=""><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>
              <ul class="tag-list">
                <li class="tag-default tag-pill tag-outline">Music</li>
                <li class="tag-default tag-pill tag-outline">Song</li>
              </ul>
            </a>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "UserProfile"
}
</script>

<style scoped>

</style>

用户设置

pages/settings

<template>
  <div class="settings-page">
    <div class="container page">
      <div class="row">

        <div class="col-md-6 offset-md-3 col-xs-12">
          <h1 class="text-xs-center">Your Settings</h1>

          <form>
            <fieldset>
              <fieldset class="form-group">
                <input class="form-control" type="text" placeholder="URL of profile picture">
              </fieldset>
              <fieldset class="form-group">
                <input class="form-control form-control-lg" type="text" placeholder="Your Name">
              </fieldset>
              <fieldset class="form-group">
                <textarea class="form-control form-control-lg" rows="8" placeholder="Short bio about you"></textarea>
              </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">
                Update Settings
              </button>
            </fieldset>
          </form>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "SettingsIndex"
}
</script>

<style scoped>

</style>

创建文章

editor/inde.vue

<template>
  <div class="editor-page">
    <div class="container page">
      <div class="row">

        <div class="col-md-10 offset-md-1 col-xs-12">
          <form>
            <fieldset>
              <fieldset class="form-group">
                <input type="text" class="form-control form-control-lg" placeholder="Article Title">
              </fieldset>
              <fieldset class="form-group">
                <input type="text" class="form-control" placeholder="What's this article about?">
              </fieldset>
              <fieldset class="form-group">
                <textarea class="form-control" rows="8" placeholder="Write your article (in markdown)"></textarea>
              </fieldset>
              <fieldset class="form-group">
                <input type="text" class="form-control" placeholder="Enter tags"><div class="tag-list"></div>
              </fieldset>
              <button class="btn btn-lg pull-xs-right btn-primary" type="button">
                Publish Article
              </button>
            </fieldset>
          </form>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "EditorIndex"
}
</script>

<style scoped>

</style>

文章详情

pages/editor

<template>
  <div class="editor-page">
    <div class="container page">
      <div class="row">

        <div class="col-md-10 offset-md-1 col-xs-12">
          <form>
            <fieldset>
              <fieldset class="form-group">
                <input type="text" class="form-control form-control-lg" placeholder="Article Title">
              </fieldset>
              <fieldset class="form-group">
                <input type="text" class="form-control" placeholder="What's this article about?">
              </fieldset>
              <fieldset class="form-group">
                <textarea class="form-control" rows="8" placeholder="Write your article (in markdown)"></textarea>
              </fieldset>
              <fieldset class="form-group">
                <input type="text" class="form-control" placeholder="Enter tags"><div class="tag-list"></div>
              </fieldset>
              <button class="btn btn-lg pull-xs-right btn-primary" type="button">
                Publish Article
              </button>
            </fieldset>
          </form>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "EditorIndex"
}
</script>

<style scoped>

</style>

处理顶部导航链接

将模板中的a链接全部替换为nuxt-link

pages/layout/index.vue

<template>
  <div>
    <!--  header -->
    <nav class="navbar navbar-light">
      <div class="container">
        <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" -->
            <nuxt-link class="nav-link" to="/" exact>
              Home
            </nuxt-link>
          </li>
          <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="/register">Sign up</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="/profile/123">
              <img class="user-pic"
                   src="https://pic1.zhimg.com/80/v2-3358e380b520aaa16d4c16bbacb7dab9_720w.jpg?source=1940ef5c">
              5coder
            </nuxt-link>
          </li>
        </ul>
      </div>
    </nav>
    <!--  header -->

    <!--    子路由  -->
    <nuxt-child/>

    <!--    footer  -->
    <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>
    <!--    footer  -->
  </div>
</template>

<script>
export default {
  name: "LayoutIndex"
}
</script>

<style scoped>

</style>

处理导航链接高亮

  • 修改nuxt.js提供的路有导航高亮,默认值为nuxt-link-active,修改为模板中定义的active官方文档

    vue.config.js

    module.exports = {
      router: {
        // 自定义路由表规则
        extendRoutes(routes, resolve) {
          // 首先清空数组,清空nuxt默认生成的路由表
          routes.splice(0)
          routes.push(...[
            {
              path: '/',
              component: resolve(__dirname, 'pages/layout'),
              children: [
                {
                  path: '', // 默认子路由
                  name: 'home',
                  component: resolve(__dirname, 'pages/home/')
                },
                {
                  path: '/login',
                  name: 'login',
                  component: resolve(__dirname, 'pages/login/')
                },
                {
                  path: '/register',
                  name: 'register',
                  component: resolve(__dirname, 'pages/login/')
                },
                {
                  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',
                  name: 'article',
                  component: resolve(__dirname, 'pages/article/')
                },
              ]
            }
          ])
        },
        linkActiveClass: 'active'  // default nav-link-active
      }
    }
    
  • 修改精确匹配,当Home中的路由为/时,默认会适用active,需要将其修改为精确匹配,这样在子组件激活时,Home不会高亮激活。(官方文档

    pages/layout/index.vue

    <li class="nav-item">
      <!-- Add "active" class when you're on that page" -->
      <nuxt-link class="nav-link" to="/" exact>
        Home
      </nuxt-link>
    </li>
    

    当前目录结构:

封装请求模块

  • 使用axios封装请求模块

  • 安装axios:yarn add axios

  • 创建目录及文件utils/request.js

    import axios from 'axios'
    
    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>

          <!--添加submit提交事件,并使用prevent取消默认提交事件-->
          <form @submit.prevent="onSubmit">
            <fieldset v-if="!isLogin" class="form-group">
              <input class="form-control form-control-lg" type="text" placeholder="Your Name">
            </fieldset>
            <!--使用v-model绑定数据-->
            <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: "LoginIndex",
  data() {
    return {
      user: {
        email: '',
        password: ''
      }
    }
  },
  computed: {
    isLogin() {
      return this.$route.name === 'login'
    }
  },
  methods: {
    async onSubmit() {
      // 提交表单请求登录
      const {data} = await request({
        method: 'POST',
        url: '/api/users/login',
        data: {
          user: this.user
        }
      })
      console.log(data)
      // TODO 保存用户登录状态

      // 跳转到首页
      this.$router.push('/')
    }
  }
}
</script>

<style scoped>

</style>

封装请求方法

为了维护方便,将请求单独再封装

创建目录及文件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
  })
}

修改login.vue中使用的登录请求

export default {
  name: "LoginIndex",
  data() {
    return {
      user: {
        email: '',
        password: ''
      }
    }
  },
  computed: {
    isLogin() {
      return this.$route.name === 'login'
    }
  },
  methods: {
    async onSubmit() {
      // 提交表单请求登录,使用刚才封装好的user.js
      const {data} = await login({
        user: this.user
      })
      console.log(data)
      // TODO 保存用户登录状态

      // 跳转到首页
      this.$router.push('/')
    }
  }
}

表单验证

使用HTML原始的验证,分别在input中添加required和修改type="emial"

<!--使用v-model绑定数据-->
<fieldset class="form-group">
  <input class="form-control form-control-lg" type="email" required placeholder="Email" v-model="user.email">
</fieldset>
<fieldset class="form-group">
  <input class="form-control form-control-lg" type="password" required placeholder="Password"
         v-model="user.password">
</fieldset>

错误处理

  • 此处处理用户登录错误时的显示内容

  • 未处理时,登录请求错误会出现如下页面:

  • 错误信息如下:

  • 使用try {} catch {err}捕获异常

    try {
      // 提交表单请求登录
      const {data} = await login({
        user: this.user
      })
      console.log(data)
      // TODO 保存用户登录状态
    
      // 跳转到首页
      this.$router.push('/')
    } catch (err) {
      // 请求失败
      console.dir(err)
      this.errors = err.response.data.errors
    }
    
  • 在data中定义errors数据:errors:{}初始化为空对象

  • 在html模板中遍历出该错误信息

    <ul class="error-messages">
      <template v-for="(messages, field) of errors">
        <li v-for="(message,index) in messages" :key="index">{{ field }} {{ message }}</li>
      </template>
    </ul>
    
  • 处理完错误后页面如下:

用户注册

由于注册需要提供用户名,所以在data中的user中添加username,并使用v-model绑定到input框中。

data() {
  return {
    user: {
      username: '',
      email: '',
      password: ''
    },
    errors: {}  // 错误信息
  }
}

注册和登录的逻辑相似,因此只需要在调用login和register时做判断即可,同时发现注册时,用户提供的密码在后端做了验证,必须大于等于8为,因此在前端也进行验证。在password字段中添加minlength='8'

<input class="form-control form-control-lg" type="password" minlength="8" required placeholder="Password" v-model="user.password">
async onSubmit() {
  try {
    // 提交表单请求登录
    const {data} = this.isLogin ? await login({
      user: this.user
    }) : await register({
      user: this.user
    })
    console.log(data)
    // TODO 保存用户登录状态

    // 跳转到首页
    this.$router.push('/')
  } catch (err) {
    // 请求失败
    console.dir(err)
    this.errors = err.response.data.errors
  }
}

存储用户登录状态

(1)初始化容器数据

// 在服务端渲染期间,运行的都是同一个实例,为了防止数据冲突,务必将state定义为一个函数,返回数据对象
// 确保每次创建实例时,state都要通过一个函数动态的创建一个对象,这样数据就不会冲突和污染
export const state = () => {
  return {
    // 当前登录用户的登录状态数据
    user: null
  }
}

export const mutations = {
  setUser(state, data) {
    state.user = data
  }
}

export const actions = {}

(2)登陆成功,将用户信息存入容器

this.$store.commit('setUser', data.user)

(3)将登陆状态持久化道Cookie中

安装js-cookie

yarn add js-cookie

使用js-cookiedata.user数据存储到cookie中(31行):Cookie.set('user', data.user)

export default {
  name: "LoginIndex",
  data() {
    return {
      user: {
        username: '',
        email: '',
        password: ''
      },
      errors: {}  // 错误信息
    }
  },
  computed: {
    isLogin() {
      return this.$route.name === 'login'
    }
  },
  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中
        Cookie.set('user', data.user)

        // 跳转到首页
        this.$router.push('/')
      } catch (err) {
        // 请求失败
        console.dir(err)
        this.errors = err.response.data.errors
      }
    }
  }
}

(4)从Cookie中获取并初始化用户登录状态

安装cookieparser

yarn add cookieparser

使用nuxtServerInit在服务端渲染期间,从cookie中获取user数据,保存到state中的user对象中

import cookieparser from 'cookieparser'

// 在服务端渲染期间,运行的都是同一个实例,为了防止数据冲突,务必将state定义为一个函数,返回数据对象
// 确保每次创建实例时,state都要通过一个函数动态的创建一个对象,这样数据就不会冲突和污染
export const state = () => {
  return {
    // 当前登录用户的登录状态数据
    user: null
  }
}

export const mutations = {
  setUser(state, data) {
    state.user = data
  }
}

export const actions = {
  // nuxtServerInit是一个特殊的action方法,尽在服务端渲染期间调用
  // 初始化容器数据,从cookie中取出来放到state中
  nuxtServerInit({commit}, {req}) {
    let user = null
    // 如果请求头中有Cookie
    if (req.headers.cookie) {
      // 使用cookieparser结构成对象
      const parsed = cookieparser.parse(req.headers.cookie)
      try{
        user = JSON.parse(parsed.user)
      } catch (e) {
        // No Valid cookie found
      }
    }
    // 提交mutation修改state状态
    commit('setUser', user)
  }
}
  • 整体逻辑为:使用mapStatestore中的state.user映射到layout.vue中,在模板中判断是否存在user,当存在时,展示Home、New Post、Settings、用户头像信息,当不存在时,只展示Home、Sign In、Sign Up
<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>
import {mapState} from 'vuex'

export default {
  name: "LayoutIndex",
  computed: {
    ...mapState(['user'])
  }
}

处理页面访问权限

当前处理逻辑只是将登陆与否过程中是否渲染了Editor等路由,然而当用户直接在url访问localhost:3000/editor等路由时,依然可以访问页面,这时就需要使用拦截器来拦截这部分请求。

路由中间件

中间件允许你定义一个自定义函数运行在一个页面或一组页面渲染之前。

您可以通过在 middleware/ 目录中创建一个文件来创建命名中间件,文件名将是中间件名称。

middleware/authenticated.js

export default function ({store, redirect}) {
  // 如果用户没有登录
  if (!store.state.user) {
    return redirect('/login')
  }
}
  • 在需要判断登录权限的页面中配置使用中间件。

editor.vue

export default {
  // 在路由匹配组件之前会先执行中间件处理
  middleware: 'authenticated',
  name: "EditorIndex"
}

同理,在settings、profile等页面也设置同样的中间件。

  • 在登录后,不允许用户重复登录注册,同样设置中间件not-authenticated.js,禁止用户在登录后再次访问登录、注册页面。

middleware/not-authenticated.js

export default function ({store, redirect}) {
  // 如果用户没有登录
  if (store.state.user) {
    return redirect('/')
  }
}
  • 在登陆注册页面加入not-authenticated中间件。

首页模块

展示公共文章列表

封装请求方法:

import request from "@/utils/request";

// 获取公共的文章列表
export const getArticles = params => {
  return request({
    method: 'GET',
    url: '/api/articles',
    params
  })
}

home/index.vue获取数据,由于需要进行SEO优化,这里使用asyncData方法获取数据:

import {getArticles} from "@/api/article";

export default {
  name: "HomeIndex",
  // 有利于SEO
  async asyncData() {
    const {data} = await getArticles()
    return {
      articles: data.articles,
      articlesCount: data.articlesCount
    }
  }
}

模板绑定:

<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.createAt }}</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>

分页处理

处理分页参数

首先定义page和limit,然后计算要请求的文章数量以及从哪一条文章开始取

export default {
  name: "HomeIndex",
  // 有利于SEO
  async asyncData() {
    let page = 1
    const limit = 20
    const {data} = await getArticles({
      limit,
      offset: (page - 1) * limit
    })
    return {
      articles: data.articles,
      articlesCount: data.articlesCount
    }
  }
}

页码处理

分页模板:

<nav>
  <ul class="pagination">
    <!--使用计算属性计算出totalPage,遍历循环出页面,并且绑定动态样式active-->
    <li class="page-item" v-for="item in totalPage" :key="item"
        :class="{
                active: item === page
                }"
        >
      <!--绑定to属性,动态传递page参数-->
      <nuxt-link class="page-link" :to="{
                                        name: 'home',
                                        query: {
                                        page: item
                                        }
                                        }">{{ item }}
      </nuxt-link>
    </li>
  </ul>
</nav>
  • 使用计算属性计算总页码

    computed: {
      totalPage() {
        return Math.ceil(this.articlesCount / this.limit)
      }
    }
    
  • 遍历生成页码列表、设置导航链接

    <nav>
      <ul class="pagination">
        <!--使用计算属性计算出totalPage,遍历循环出页面,并且绑定动态样式active-->
        <li class="page-item" v-for="item in totalPage" :key="item"
            :class="{
                    active: item === page
                    }"
            >
          <!--绑定to属性,动态传递page参数-->
          <nuxt-link class="page-link" :to="{
                                            name: 'home',
                                            query: {
                                            page: item
                                            }
                                            }">{{ item }}
          </nuxt-link>
        </li>
      </ul>
    </nav>
    
  • 相应query参数变化

watchQuery: ['page'],

获取标签列表(Popular Tags)

  • 封装请求方法api/tag.js
import request from "@/utils/request";

// 获取文章标签列表
export const getTags = () => {
  return request({
    method: 'GET',
    url: '/api/tags',
  })
}
  • home/index.vue中获取数据
export default {
  name: "HomeIndex",
  // 有利于SEO
  watchQuery: ['page'],
  async asyncData({query}) {
    // 从url中获取页码:localhost:3000?page=3
    const page = Number.parseInt(query.page || 1)
    const limit = 20
    const {data} = await getArticles({
      limit,
      offset: (page - 1) * limit
    })
    const {data: tagData} = await getTags()  // 获取tags数据

    return {
      articles: data.articles,
      articlesCount: data.articlesCount,
      limit,
      page,
      tags: tagData.tags  // 返回tags数据给页面模板
    }
  },
  computed: {
    totalPage() {
      return Math.ceil(this.articlesCount / this.limit)
    }
  }
}
  • 遍历获取到的tags,渲染到页面
<div class="tag-list">
  <a href="" class="tag-pill tag-default" v-for="item in tags" :key="item" v-if="item">{{ item }}</a>
</div>

优化数据请求

前面请求的文章列表数据和标签列表数据在业务上并没有互相依赖的关系,因此可以将其从串行执行请求数据优化为并行执行请求数据,通过并行可以提高请求加载的速度。

async asyncData({query}) {
  // 从url中获取页码:localhost:3000?page=3
  const page = Number.parseInt(query.page || 1)
  const limit = 20
  // 获取返回结果并解构 Promise.all 方法返回值未数组
  const [articlesResponse, tagResponse] = await Promise.all([
    getArticles({
      limit,
      offset: (page - 1) * limit
    }),
    getTags()
  ])
  // 解构结果值
  const {articles, articlesCount} = articlesResponse.data
  const {tags} = tagResponse.data

  return {
    articles,
    articlesCount,
    limit,
    page,
    tags
  }
},

标签列表链接和数据

  • 处理标签列表链接,类似于分页页码的处理;在标签上绑定查询参数?tag='something'

    <nuxt-link :to="{
          name: 'home',
          query: {
            tag: item
          }
      }" class="tag-pill tag-default" v-for="item in tags" :key="item" v-if="item">{{ item }}</nuxt-link>
    
  • 搭配?page=3?tag=‘something’

    <nuxt-link class="page-link" :to="{
        name: 'home',
        query: {
          page: item,
          tag: $route.query.tag
        }
    }">{{ item }}
    </nuxt-link>
    

标签高亮及链接(Tab)

  • 业务逻辑:当用户登录后,显示Your Feed, 否则不显示Your Feed,只显示Global Feed

    • 获取user,判断user,是否展示Your Feed

      <li v-if="user" class="nav-item">
        <nuxt-link class="nav-link" ...>Your Feed</nuxt-link>
      </li>
      
  • 业务逻辑:点击右侧Popular Tag时,动态显示#{{ tag }}tab导航栏

    • 判断当前url中是否有tag,如果有则动态的渲染tab导航栏

      <li v-if="tag">
      	...
      </li>
      
      • 在asyncData中需要返回tag属性

        async asyncData({query}) {
          // 从url中获取页码:localhost:3000?page=3
          const page = Number.parseInt(query.page || 1)
          const limit = 20
          const {tag} = query
          const [articlesResponse, tagResponse] = await Promise.all([
            getArticles({
              limit,
              offset: (page - 1) * limit,
              tag  // 查询tag
            }),
            getTags()
          ])
          const {articles, articlesCount} = articlesResponse.data
          const {tags} = tagResponse.data
        
          return {
            articles,
            articlesCount,
            limit,
            page,
            tags,
            tag,  // 返回tag
            tab: query.tab || 'global_feed'
          }
        },
        
    • Popular Tag query查询参数中添加tag属性

      <div class="tag-list">
        <nuxt-link :to="{
              name: 'home',
              query: {
                tag: item,  // 添加tag查询参数
                tab: 'tag'
              }
            }" class="tag-pill tag-default" v-for="item in tags" :key="item" v-if="item">{{ item }}
        </nuxt-link>
      </div>
      
  • 动态绑定Your Feed、Global Feed、#tag的active样式

    • 给tab栏添加查询参数tab: 'your_feed'tab: 'global_feed'tab: tag

    • 精确匹配exactwatchQueryt中添加tab

      <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'
        }
      }" href="">Your Feed
        </nuxt-link>
      </li>
      <li class="nav-item">
        <nuxt-link
            class="nav-link"
            :class="{
              active: tab === 'global_feed'
            }"
            exact
            :to="{
              name: 'home',
              query: {
                tab: 'global_feed'
              }
            }">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>
      
      watchQuery: ['page', 'tag', 'tab'],
      
      • 在热门标签Popular Tags中,选择页码,发现Tab并没有被激活,所以需要在页码中家也如查询参数query,tab:tab

展示关注文章列表

Your Feed中的数据渲染到该Tab

  • 首先在asyncData中解构出store对象,从store对象中获取user,用于传递到接口服务器,在接口中先手动写入用户token
// 获取关注的的文章列表
export const getFeedArticles = params => {
  return request({
    method: 'GET',
    url: '/api/articles/feed',
    // 注意数据格式
    headers: {
      Authorization: `Token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTkwMzIyLCJ1c2VybmFtZSI6IjVjb2RlciIsImV4cCI6MTYzMjUzNTA0NH0.i0tpsAKIB-462Vg_dOyCABZNcFwNMqRtvQ-jzoDTY6k`
    },
    params
  })
}
  • 根据用户登录状态、tab是否为your_feed两个条件决定加载全部文章或者关注的文章
import {getArticles, getFeedArticles} from "@/api/article";
...
async asyncData({query, store}) {
  // 从url中获取页码:localhost:3000?page=3
  const page = Number.parseInt(query.page || 1)
  const limit = 20
  const {tag} = query
  const tab = query.tab || 'global_feed'
  // 判断加载全部文章或者关注文章
  const loadArticles = store.state.user && tab === 'your_feed'
      ? getFeedArticles
      : getArticles


  const [articlesResponse, tagResponse] = await Promise.all([
    loadArticles({
      limit,
      offset: (page - 1) * limit,
      tag,
    }),
    getTags()
  ])

  const {articles, articlesCount} = articlesResponse.data
  const {tags} = tagResponse.data

  return {
    articles,
    articlesCount,
    limit,
    page,
    tags,
    tag,
    tab: query.tab || 'global_feed'
  }
},

统一添加数据token

在上一步中,我们手动设置的用户的token值,但是在实际的业务中需要动态的添加token值。也就是说需要在store中获取到state.user.token。但是在request.js中我们无法获取到store上下文对象。Nuxt.js为我们提供了一个插件机制,插件机制可以让我们在真正请求之前拦截到请求,并在请求中可以获取上下文对象(query、params、req、res、app、store…)

插件机制使用如下:

在项目根目录创建目录plugins以及文件request.js,导出默认成员,代码如下:

plugins/request.js

/*
* 基于 axios 封装的请求模块
* */

import axios from 'axios'

// 创建请求对象
export const request = axios.create({
  baseURL: 'https://conduit.productionready.io'
})


// 插件导出函数必须作为default成员,而default成员只有一个
// 通过插件机制获取到上下文对象(query、params、req、res、app、store...)
export default ({store}) => {
  console.log(123)
  // 请求拦截器
  // 任何请求都要经过请求拦截器
  // 我们可以在请求拦截器中做一些公共的业务处理,例如设置token
  request.interceptors.request.use(function (config) {
    // 请求就会经过这里
    // 拿到用户的token
    const {user} = store.state
    if (user && user.token) {
      config.headers.Authorization = `Token ${user.token}`
    }
    // 返回config请求配置对象
    return config
  }, function (error) {
    // 如果请求失败(此时请求还没有发出去)就会进入这里
    return Promise.reject(error)
  })
}

此外,当需要使用插件时,还需要在nuxt.config.js中进行注册插件,代码如下:

module.exports = {
  router: {
    ...
  },
  // 注册插件
  plugins: [
    '~/plugins/request.js'
  ]
}

export default导出默认成员时,可以使用对象解构,只解构出我们需要的store对象,然后在判断user以及user.token,当条件成立后,在config中配置headers,按照API文档就进行设置。

const {user} = store.state
if (user && user.token) {
  config.headers.Authorization = `Token ${user.token}`
}

此时,我们就不需要再使用utils/request.js中的方法了,需要在api中的所有文件(articles.jstag.jsuser.js)中替换导入

import { request } from "@/plugins/request";

此时,在我们home/index.vue中的所有请求都会使用plugins/request.js,所有请求也会经过拦截器,进而统一设置用户token

日期格式处理

在线demo中,日期的展现形式是:简写月份 日期, 年份,所以我们需要将获取到的articles中的createAt字段改变为此格式。这里推荐一个类似于moment.js的插件dayjs.js(Github)官方文档

Day.js 是一个轻量的处理时间和日期的 JavaScript 库,和 Moment.js 的 API 设计保持完全一样. 如果您曾经用过 Moment.js, 那么您已经知道如何使用 Day.js

使用Vue中的全局过滤器,将日期格式化,这样可以更大限度的重用代码。同样需要在plugins目录中新建dayjs.js文件(文件名随意),代码如下:

plugins/dayjs.js

import Vue from 'vue'
import dayjs from 'dayjs'

// {{ 表达式 | 过滤器 }}
// filter第一个参数为过滤器名称
// filter第二个参数为函数,函数中的value为表达式返回的值
Vue.filter('date', (value, format = 'YYYY-MM-DD HH:mm:ss') => {
  return dayjs(value).format(format)
})

在pages/home/index.vue中使用管道附链接,并传入参数:

<span class="date">{{ article.createAt | date('MMM DD, YYYY') }}</span>

文章点赞

接下来处理文章点赞功能,业务逻辑:

  • 未点赞状态下,点击为点赞,数量加1
  • 点赞状态下,点击为取消点赞,数量减1

整体流程

  • 新增两个数据接口

api/articles.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`
  })
}
  • pages/home/index.vue中的点赞按钮中绑定事件
<button class="btn btn-outline-primary btn-sm pull-xs-right"
        :class="{
                active: article.favorited
                }" @click="onFavorite(article)"
                     
        >...</button>
  • vm实例中添加methods
methods: {
  async onFavorite(article) {
    // 如果已经点赞了,则取消点赞,否则则添加点赞
    if (article.favorited) {
      await deleteFavorite(article.slug)
      // 处理视图
      article.favorited = false
      // 数量减1
      article.favoritesCount += -1
    } else {
      await addFavorite(article.slug)
      article.favorited = true
      // 数量加1
      article.favoritesCount += 1
    }
  }
}
  • 处理在点赞和取消点赞过程中的pedding状态,防止用户快速点击按钮,实现思路为动态更改buttondisabled属性

    • 动态的在接收返回的articles数据中新增favoriteDisabled属性

      articles.forEach(articles => articles.favoriteDisabled = false)
      

pages/home/index.vue

<button class="btn btn-outline-primary btn-sm pull-xs-right"
        :class="{
          active: article.favorited
        }" @click="onFavorite(article)"
        :disabled="article.favoriteDisabled"
>
  <i class="ion-heart"></i>
  {{ article.favoritesCount }}
</button>
  • 点赞过程中动态的绑定disabled属性
methods: {
  async onFavorite(article) {
    // disabled为true
    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
    }
    // disabled为false
    article.favoriteDisabled = false
  }
}

文章详情

业务介绍:

  1. 展示文章详情内容
    1. 文章标题
    2. 作者信息
    3. 点赞
    4. 正文
  2. 文章评论功能
    1. 发布评论
  • 文章详情数据接口封装

    api/articles.js

    // 获取文章详情
    export const getArticle = slug => {
      return request({
        method: 'GET',
        url: `/api/articles/${slug}`,
      })
    }
    
  • 获取数据

    pages/article/index.vue

    import {getArticle} from "@/api/article";
    
    export default {
      name: "ArticleIndex",
      // 通过对象结构,拿出params,进而拿到params.slug,即文章id
      async asyncData({params}) {
        const {data} = await getArticle(params.slug)
        return {
          article: data.article
        }
      }
    }
    
  • 动态渲染文章数据

    这里先只渲染文章title和文章body,并且文章body不进行markdown转换

    <h1>{{ article.title }}</h1>
    
    ...
    
    <div class="row article-content">
      <div class="col-md-12">
        {{ article.body }}
      </div>
    </div>
    

把Markdown转为HTMl

使用第三方插件markdown-it,该插件可以将markdown语法转换为HTML。在获取到article.body后,使用该插件方法将其转为HTML

import {getArticle} from "@/api/article";
import MarkdownIt from 'markdown-it'

export default {
  name: "ArticleIndex",
  async asyncData({params}) {
    const {data} = await getArticle(params.slug)
    const {article} = data
    const md = new MarkdownIt()
    article.body = md.render(article.body)
    return {
      article
    }
  }
}

在模板中,使用v-html指令填充article.body

<div class="col-md-12" v-html="article.body"></div>

展示文章作者相关信息

文章详情页面中有两部分功能相似:

文章作者信息、关注按钮、点赞按钮

  • 首先封装组件

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 :to="{
        name: 'profile',
        params: {
          username: article.author.username
        }
      }" class="author">{{ article.author.username }}
      </nuxt-link>
      <span class="date">{{ article.createdAt | date('MMM DD, YYYY') }}</span>
    </div>
    <button
        class="btn btn-sm btn-outline-secondary"
        :class="{
          active: article.author.following
        }"
    >
      <i class="ion-plus-round"></i>
      Follow Eric Simons <span class="counter">({{ article.followCount }})</span>
    </button>
    <button
        class="btn btn-sm btn-outline-primary"
        :class="{
          active: article.favorited
        }"
        @click="onFavorite(article)"
    >
      <i class="ion-heart"></i>
      Favorite Post <span class="counter">({{ article.favoritesCount }})</span>
    </button>
  </div>
</template>

<script>
import {addFavorite, deleteFavorite} from "@/api/article";
export default {
  name: "ArticleMeta",
  props: {
    article: {
      type: Object,
      required: true
    }
  },
  methods: {
    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
    }
  }

}
</script>

<style scoped>

</style>
  • article/index.vue中的两个地方使用组件,并且传递articlearticle-meta.vue中,article-meta.vue使用props接受article数据,如上方代码
<article-meta :article="article"/>
  • 动态遍历渲染article以及作者相关信息,如封装组件中的代码
  • 动态绑定点赞按钮事件,与之前的home/index.vue中的用法相同
<button
    class="btn btn-sm btn-outline-primary"
    :class="{
      active: article.favorited
    }"
    @click="onFavorite(article)"
>
  <i class="ion-heart"></i>
  Favorite Post <span class="counter">({{ article.favoritesCount }})</span>
</button>
methods: {
  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
  }
}

TODO 关注按钮事件,其原理与点赞按钮相同,封装API请求,点击按钮,判断当前状态,更改数据

设置页面meta优化SEO

  • 修改页面标题,希望在页面标题中出现文章的标题

特定页面的Meta标签用法

Nuxt.js 使用了 vue-meta 更新应用的 头部标签(Head)html 属性

  • 类型: ObjectFunction

使用 head 方法设置当前页面的头部标签。

head 方法里可通过 this 关键字来获取组件的数据,你可以利用页面组件的数据来设置个性化的 meta 标签。

<template>
  <h1>{{ title }}</h1>
</template>

<script>
  export default {
    data() {
      return {
        title: 'Hello World!'
      }
    },
    head() {
      return {
        title: this.title,
        meta: [
          {
            hid: 'description',
            name: 'description',
            content: 'My custom description'
          }
        ]
      }
    }
  }
</script>

注意:为了避免子组件中的 meta 标签不能正确覆盖父组件中相同的标签而产生重复的现象,建议利用 hid 键为 meta 标签配一个唯一的标识编号。

article/index.vue

import {addFavorite, deleteFavorite} from "@/api/article";

export default {
  name: "ArticleMeta",
  props: {
    article: {
      type: Object,
      required: true
    }
  },
  methods: {
    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
    }
  },
  head() {
    return {
      title: `${this.article.title} - RealWorld`,
      meta: [
        {
          hid: 'description',
          name: 'description',
          content: this.article.description
        }
      ]
    }
  }
}

显示效果

文章评论

  • 首先封装组件article-comments.vue,将评论部分抽离出来单独获取数据。
  • 使用Vue生命周期函数mounted(此部分不需要SEO优化,因此采用客户端渲染)加载数据
  • 使用封装好的组件,传递相关的文章对象article
  • 渲染遍历数据

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
            :to="{
              name: 'profile',
              params: {
                username: comment.author.username
              }
            }"
            class="comment-author">
          <img :src="comment.author.image" class="comment-author-img"/>
        </nuxt-link>
        &nbsp;
        <nuxt-link
            :to="{
              name: 'profile',
              params: {
                username: comment.author.username
              }
            }"
            class="comment-author">{{ 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)
    console.log(data)
    this.comments = data.comments
  }
}
</script>

<style scoped>

</style>

发布部署

Nuxt.js 提供了两种发布部署应用的方式:服务端渲染应用部署 和 静态应用部署。

部署 Nuxt.js 服务端渲染的应用不能直接使用 nuxt 命令,而应该先进行编译构建,然后再启动 Nuxt

服务,可通过以下两个命令来完成:

nuxt build
nuxt start

打包

Nuxt.js 提供了一系列常用的命令, 用于开发或发布部署。

命令描述
nuxt启动一个热加载的Web服务器(开发模式)localhost:3000
nuxt build利用Webpack编译应用,压缩js和css资源(发布用)
nuxt start以生产模式启动一个Web服务器(需要先执行nuxt build
nuxt gnerate编译应用,并依据路由配置生成对应的HTML文件(用于静态站点的部署)

如果使用了 Koa/Express 等 Node.js Web 开发框架,并使用了 Nuxt 作为中间件,可以自定义 Web 服务器的启动入口:

命令描述
NODE_ENV=development nodemon server/index.js启动一个热加载的自定义Web服务器(开发模式)
NODE_ENV=production node server/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 <command> 来执行相应的命令。如: npm run dev

提示: 要将参数传递给npm命令,您需要一个额外的-- 脚本名称(例如: npm run dev --参数 --spa )

最简单的部署方式

  • 配置Host + Port

nuxt.config.js

/*
* Nuxt.js配置文件
* */

module.exports = {
  router: {
    // 自定义路由表规则
    extendRoutes(routes, resolve) {
      // 首先清空数组,清空nuxt默认生成的路由表
      routes.splice(0)
      routes.push(...[
        {
          path: '/',
          component: resolve(__dirname, 'pages/layout'),
          children: [
            {
              path: '', // 默认子路由
              name: 'home',
              component: resolve(__dirname, 'pages/home/')
            },
            {
              path: '/login',
              name: 'login',
              component: resolve(__dirname, 'pages/login/')
            },
            {
              path: '/register',
              name: 'register',
              component: resolve(__dirname, 'pages/login/')
            },
            {
              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',
              name: 'article',
              component: resolve(__dirname, 'pages/article/')
            },
          ]
        }
      ])
    },
    linkActiveClass: 'active',  // default nav-link-active,
  },
  // 注册插件
  plugins: [
    '~/plugins/request.js',
    '~/plugins/dayjs.js'
  ],
  head: {
    meta: [
      {charset: 'utf-8'}
    ]
  },
  server: {
    host: '0.0.0.0',
    port: 3000
  }
}
  • 压缩发布包

  • 把发布包传到服务器

    需要传到服务器的文件、目录

    • .nuxt
    • static
    • nuxt.config.js
    • package.json
    • package-lock.json

    windows中压缩上述文件,得到realworld-nuxtjs.zip文件

    使用src -r realworld-nuxtjs.zip 用户名@服务器ip:服务器目录将压缩包传到服务器

  • 解压

    • 使用unzip realworld-nuxtjs.zip解压文件
  • 安装依赖

    • 安装依赖前一定要先安装node.js

      • 安装方式:
        1. 下载linux安装包https://nodejs.org/dist/v14.17.3/node-v14.17.3-linux-x64.tar.xz
        2. 同上面步骤,scp传递文件到服务器usr/local目录
        3. 使用tar xf node-v14.17.3-linux-x64.tar.xz解压
        4. cd node-v14.17.3-linux-x64
        5. ./bin/node -v 查看node版本
        6. 设置软连接
          1. ln -s /usr/local/node-v14.17.3-linux-x64/bin/npm /usr/local/bin
          2. ln -s /usr/local/node-v14.17.3-linux-x64/bin/node /usr/local/bin
        7. 安装yarn
          1. npm install yarn -g
        8. 设置yarn软链接
          1. ln -s /usr/local/node-v14.17.3-linux-x64/bin/yarn /usr/local/bin
        9. 使用yarn命令,安装依赖
  • 启动服务

    • 使用命令:yarn start启动服务

  • 浏览器访问:服务器IP:3000

    • 前提需要将3000端口的防火墙设置打开

使用PM2启动Node服务

使用上述方式启动服务,命令行被占用,当命令行被关闭后,服务就被关闭了,所以使用PM2,PM2专门用来管理node进程的应用。

  • GitHub仓库地址:https://github.com/Unitech/pm2

  • 官方文档:https://pm2.keymetrics.io/docs/usage/quick-start/

  • 安装:yarn global add pm2

  • 启动:pm2 start 脚本路径

    • 可以使用pm2 start yarn – start方式启动,-- start为传参的方式

  • 关闭pm2

    • pm2 stop 0

PM2常用命令

自定化部署介绍

传统的部署方式

现代化部署方式(CI/CD:持续集成、持续部署)

准备自动部署内容

  • 使用GitHub Actions实现自动部署

  • 环境准备

    • Linux服务器
    • 把代码提交到GitHub远程仓库
  • 配置GitHub Access Token

  • 生成:https://github.com/settings/tokens

  • 配置到项目的Secrets中:https://github.com/5coder-leo/realworld/settings/secrets/actions

配置GitHub Actions执行脚本

  • 在项目根目录创建.github/workflows目录

  • 下载main.ymlworkflows目录中

    main.yml

    name: Publish And Deploy Demo
    on:
      push:
        tags:
          # 标签以v开头,就会触发以下脚本
          - 'v*'
    
    jobs:
      build-and-deploy:
        runs-on: ubuntu-latest  # 运行环境
        steps:
    
          # 下载源码
          - name: Checkout
            uses: actions/checkout@master
    
          # 打包构建
          - name: Build
            uses: actions/setup-node@master
          - run: npm install
          - run: npm run build
          - run: tar -zcvf release.tgz .nuxt static nuxt.config.js package.json package-lock.json pm2.config.json
    
          # 发布 Release
          - name: Create Release
            id: create_release
            uses: actions/create-release@master
            env:
              GITHUB_TOKEN: ${{ secrets.TOKEN }}
            with:
              tag_name: ${{ github.ref }}
              release_name: Release ${{ github.ref }}
              draft: false
              prerelease: false
    
          # 上传构建结果到 Release
          - name: Upload Release Asset
            id: upload-release-asset
            uses: actions/upload-release-asset@master
            env:
              GITHUB_TOKEN: ${{ secrets.TOKEN }}
            with:
              upload_url: ${{ steps.create_release.outputs.upload_url }}
              asset_path: ./release.tgz
              asset_name: release.tgz
              asset_content_type: application/x-tgz
    
          # 部署到服务器
          - name: Deploy
            uses: appleboy/ssh-action@master
            with:
              host: ${{ secrets.HOST }}
              username: ${{ secrets.USERNAME }}
              password: ${{ secrets.PASSWORD }}
              port: ${{ secrets.PORT }}
              script: |
                cd /home/leo/realworld-nuxtjs
                wget https://github.com/5coder-leo/realworld/releases/latest/download/release.tgz -O release.tgz
                tar zxvf release.tgz
                npm install --production
                pm2 reload pm2.config.json
    
    
  • 修改配置

  • 配置PM2配置文件

    pm2.config.json

    {
      "apps": [
        {
          "name": "RealWorld",
          "script": "npm",
          "args": "start"
        }
      ]
    }
    
  • 提交更新

    git add .
    git commit -m "更新测试"
    git push -u origin master
    
    # 开始设立tag标签
    git tag v0.1.0
    git push origin v0.1.0
    
  • 查看自动部署状态

  • 访问网址:http://106.75.130.241:3000/

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-07-29 11:32:24  更:2021-07-29 11:34:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/6 21:39:46-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码