Nuxt综合案例实现

前言

我们要实现的Demo[小伙伴可以自行点进去看看哈~]
https://demo.realworld.io/#/

以下是当前Demo的相关资源列举

  • GitHub 仓库 https://github.com/gothinkster/realworld
  • 接口文档 https://github.com/gothinkster/realworld/tree/master/api
  • 页面模板 https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md

准备工作

项目初始化

mkdir [projectname]
cd [projectname]
yarn init -y
yarn add nuxt
---
vi package.json
insert content
"scripts": {
    "dev": "nuxt"
},
esc  :wq
---
mkdir pages
touch index.vue

index.vue

<template>
  <div class="home">
    home
  </div>
</template>

<script>
export default {
  name: 'home',
};
</script>

<style lang="scss" scoped></style>

yarn dev

打开响应端口页面,看到页面展示内容就说明我们的项目初始化完成啦~

导入页面模板

导入样式资源

touch app.html

  1. App.html 默认模板 copy 到 app.html 中
  2. realworld资源 资源部分导入到文件中
  3. 重启服务,F12验证资源加载

资源本土化方法

open https://www.jsdelivr.com/
search ionicons
找到对应的版本[2.0.1]下的css/ionicons.min.css
copy [链接](https://cdn.jsdelivr.net/npm/ionicons@2.0.1/css/ionicons.min.css)
将href替换成我们新的资源地址
open //demo.productionready.io/main.css
copy 内容
新建 static/main.css
将内容粘贴进去
替换资源引入'/main.css'
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head {{ HEAD_ATTRS }}>
    {{ HEAD }}
    <!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on -->
    <link
      href="https://cdn.jsdelivr.net/npm/ionicons@2.0.1/css/ionicons.min.css"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic"
      rel="stylesheet"
      type="text/css"
    />
    <!-- Import the custom Bootstrap 4 theme from our hosted CDN -->
    <!-- <link rel="stylesheet" href="//demo.productionready.io/main.css" /> -->
    <link rel="stylesheet" href="/main.css" />
  </head>
  <body {{ BODY_ATTRS }}>
    {{ APP }}
  </body>
</html>

配置布局组件

  1. 创建文件

    mkdir pages/layouttouch
    pages/layout/index.vue
    
  2. realworld资源 中Header/Footer 部分copy到文件中

  3. 配置自定义路由规则

    touch nuxt.config.js

    module.exports = {
      router: {
        // 自定义路由规则
        extendRoutes(routes, resolve) {
          // 清除 Nuxt.js 基于 pages 目录默认生成的路由表规则
          routes.splice(0);
    
          routes.push(
            ...[
              {
                name: 'layout',
                path: '/',
                component: resolve(__dirname, 'pages/layout/'),
              },
            ]
          );
        },
      },
    };
    
  4. 创建主页面相关

    mkdir pages/home
    touch pages/home/index.vue
    

    realworld资源 中Home 部分copy到文件中

    配置路由

    module.exports = {
      router: {
        // 自定义路由规则
        extendRoutes(routes, resolve) {
          // 清除 Nuxt.js 基于 pages 目录默认生成的路由表规则
          routes.splice(0);
    
          routes.push(
            ...[
              {
                name: 'layout',
                path: '/',
                component: resolve(__dirname, 'pages/layout/'),
                children: [
                  {
                    path: '', // 默认子路由
                    name: 'home',
                    component: resolve(__dirname, 'pages/home/'),
                  },
                ],
              },
            ]
          );
        },
      },
    };
    

配置页面组件

导入登录注册页面

  1. 创建pages/login/index.vue

  2. 从资源 Login/Register 中将内容拷贝到文件中

  3. 相关路由配置

    children: [
      {
        path: '/login',
        name: 'login',
        component: resolve(__dirname, 'pages/login/')
      },
      {
        path: '/register',
        name: 'register',
        component: resolve(__dirname, 'pages/login/')
      },
    ],
    
  4. 处理页面pages/login/index.vue

    <template>
      <div class="auth-page">
        <div class="container page">
          <div class="row">
            <div class="col-md-6 offset-md-3 col-xs-12">
              <h1 class="text-xs-center">{{ isLogin ? 'Sign in' : 'Sign up' }}</h1>
              <p class="text-xs-center">
                <nuxt-link v-if="isLogin" to="/register"
                  >Need an account?</nuxt-link
                >
                <nuxt-link v-else to="/login">Have an account?</nuxt-link>
              </p>
    
              <ul class="error-messages">
                <li>That email is already taken</li>
              </ul>
    
              <form>
                <fieldset v-if="!isLogin" class="form-group">
                  <input
                    class="form-control form-control-lg"
                    type="text"
                    placeholder="Your Name"
                  />
                </fieldset>
                <fieldset class="form-group">
                  <input
                    class="form-control form-control-lg"
                    type="text"
                    placeholder="Email"
                  />
                </fieldset>
                <fieldset class="form-group">
                  <input
                    class="form-control form-control-lg"
                    type="password"
                    placeholder="Password"
                  />
                </fieldset>
                <button class="btn btn-lg btn-primary pull-xs-right">
                  {{ isLogin ? 'Sign in' : 'Sign up' }}
                </button>
              </form>
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      name: 'Login',
      computed: {
        isLogin() {
          return this.$route.name === 'login';
        },
      },
    };
    </script>
    
    <style lang="scss" scoped></style>
    

导入剩余页面

按照规则配置页面的内容以及路由即可

  1. 创建pages/profile/index.vue
  2. 从资源 Login/Register 中将内容拷贝到文件中
  3. 相关路由配置

处理导航链接

1. 处理导航链接

这里主要将顶部导航的a链接替换成nuxt-link 处理到正确的路由

处理完成的页面pages/layout/index.vue

<template>
  <div>
    <!-- 顶部导航栏 -->
    <nav class="navbar navbar-light">
      <div class="container">
        <nuxt-link class="navbar-brand" to="/">conduit</nuxt-link>
        <ul class="nav navbar-nav pull-xs-right">
          <li class="nav-item">
            <!-- Add "active" class when you're on that page" -->
            <nuxt-link class="nav-link active" to="/">Home</nuxt-link>
          </li>
          <li class="nav-item">
            <nuxt-link class="nav-link" to="/editor">
              <i class="ion-compose"></i>&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="/login">Sign in</nuxt-link>
          </li>
          <li class="nav-item">
            <nuxt-link class="nav-link" to="/register">Sign up</nuxt-link>
          </li>
          <li class="nav-item">
            <nuxt-link class="nav-link" to="/profile/1">
              <img class="user-pic" src="" />
              crystalangel
            </nuxt-link>
          </li>
        </ul>
      </div>
    </nav>
    <!-- /顶部导航栏 -->
    <!-- 子路由 -->
    <nuxt-child />
    <!-- /子路由 -->

    <!-- 底部 -->
    <footer>
      <div class="container">
        <a href="/" class="logo-font">conduit</a>
        <span class="attribution">
          An interactive learning project from
          <a href="https://thinkster.io">Thinkster</a>. Code &amp; design
          licensed under MIT.
        </span>
      </div>
    </footer>
    <!-- /底部 -->
  </div>
</template>

2. 处理导航链接高亮

active className处理

nuxt.config.js

module.exports = {
	router: {
	    linkActiveClass: 'active',
	}
}

pages/layout/index.vue
修改home 的nuxt-link

<nuxt-link class="nav-link" exact to="/">Home</nuxt-link>

封装请求模块

1. 安装依赖 yarn add axios
2. 创建 utils/request.js 文件

import axios from 'axios';
// https://conduit.productionready.io
const request = axios.create({
  baseUrl: 'https://conduit.productionready.io',
});

export default request;

登录注册功能实现

基本登录功能

登录接口文档

  1. 绑定表单数据
  2. 注册点击登录事件
  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">
            <nuxt-link v-if="isLogin" to="/register"
              >Need an account?</nuxt-link
            >
            <nuxt-link v-else to="/login">Have an account?</nuxt-link>
          </p>

          <ul class="error-messages">
            <li>That email is already taken</li>
          </ul>

          <form @submit.prevent="onSubmit">
            <fieldset v-if="!isLogin" class="form-group">
              <input
                class="form-control form-control-lg"
                type="text"
                placeholder="Your Name"
              />
            </fieldset>
            <fieldset class="form-group">
              <input
                class="form-control form-control-lg"
                type="text"
                placeholder="Email"
                v-model="user.email"
              />
            </fieldset>
            <fieldset class="form-group">
              <input
                class="form-control form-control-lg"
                type="password"
                placeholder="Password"
                v-model="user.password"
              />
            </fieldset>
            <button class="btn btn-lg btn-primary pull-xs-right">
              {{ isLogin ? 'Sign in' : 'Sign up' }}
            </button>
          </form>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import request from '@/utils/request';
export default {
  name: 'Login',
  computed: {
    isLogin() {
      return this.$route.name === 'login';
    },
  },
  data() {
    return {
      user: {
        email: '',
        password: '',
      },
    };
  },
  methods: {
    async onSubmit() {
      const { data } = await request({
        method: 'POST',
        url: '/api/users/login',
        data: { user: this.user },
      });
      console.log(data);
      if (data) this.$router.push('/');
    },
  },
};
</script>

封装请求方法

在项目里面的接口请求,为了维护的方便,我们会将接口封装到统一的目录下

  1. 新建 api/user.js 文件
  2. 封装接口
  3. 替换页面使用方法

api/user.js

import request from '@/utils/request';

export const login = (data) =>
  request({
    method: 'POST',
    url: '/api/users/login',
    data,
  });

pages/login/index.vue

import { login } from '@/api/user';
export default {
  methods: {
    async onSubmit() {
      const { data } = await login({ user: this.user });
      console.log(data);
      if (data) this.$router.push('/');
    },
  },
}

表单验证

我们这里专注于体验Nuxt 所以 验证的地方直接使用原生的input 验证

<fieldset v-if="!isLogin" class="form-group">
  <input
    class="form-control form-control-lg"
    type="text"
    placeholder="Your Name"
    required
  />
</fieldset>
<fieldset class="form-group">
  <input
    class="form-control form-control-lg"
    type="email"
    placeholder="Email"
    v-model="user.email"
    required
  />
</fieldset>
<fieldset class="form-group">
  <input
    class="form-control form-control-lg"
    type="password"
    placeholder="Password"
    v-model="user.password"
    required
  />
</fieldset>

错误处理

我们当前的情况是:如果用户输入错误,页面会直接跳到 Request failed 页面
正常情况下我们应该在用户输入错误之后将错误信息显示到页面中,处理方法就是将我们提交方法try catch起来,然后捕获错误信息,将错误信息展示到页面中

<template>
  ...
  <ul class="error-messages">
    <template v-for="(messages, field) in errors">
      <li v-for="(message, index) in messages" :key="index">
        {{ field }} {{ message }}
      </li>
    </template>
  </ul>
  ...
</template>
<script>
export default {
  ...
  data() {
    return {
      ...
      errors: {}, // 错误信息
    };
  },
  methods: {
    async onSubmit() {
      try {
        const { data } = await login({ user: this.user });
        console.log(data);
        if (data) this.$router.push('/');
      } catch (error) {
        // console.dir(error);
        this.errors = error.response.data.errors;
      }
    },
  },
};
</script>

用户注册

用户注册功能整体逻辑和登录大同小异

  1. 绑定表单用户名信息
  2. 完善密码长度校验
  3. sign up 请求接口信息
  4. 请求成功跳转到首页
  5. 请求失败将失败信息展示到页面

pages/login/index.vue

<template>
 ...
 <fieldset v-if="!isLogin" class="form-group">
   <input
     class="form-control form-control-lg"
     type="text"
     placeholder="Your Name"
     required
     v-model="user.username"
   />
 </fieldset>
 ...
 <fieldset class="form-group">
   <input
     class="form-control form-control-lg"
     type="password"
     placeholder="Password"
     v-model="user.password"
     minlength="8"
     required
   />
 </fieldset>
 ... 
</template>

<script>
import { login, register } from '@/api/user';
export default {
  ...
  data() {
    return {
      user: {
        username: '',
        email: '',
        password: '',
      },
      errors: {}, // 错误信息
    };
  },
  methods: {
    async onSubmit() {
      try {
        const { data } = this.isLogin
          ? await login({ user: this.user })
          : await register({ user: this.user });
        console.log(data);
        this.$router.push('/');
      } catch (error) {
        // console.dir(error);
        this.errors = error.response.data.errors;
      }
    },
  },
};
</script>

api/user.js

import request from '@/utils/request';

export const register = (data) =>
  request({
    method: 'POST',
    url: '/api/users',
    data,
  });

存储登录状态

登录状态我们需要在多个页面内使用,所以这里我们需要一个容器将其存储起来,由于服务环境下依然需要这个状态,所以实现起来跟VueJS中有些不同
这里可以先看一下官方示例的实现,我们的实现过程和官方的其实是一致的

  1. 初始化容器: 创建 store/index.js
  2. 将登录状态存储到容器中
  3. 登录状态持久化处理(防止页面刷新之后状态丢失的问题)【将状态存放到cookie中,方便在服务器中依然可以拿到状态,需要安装依赖 js-cookie\cookieparser】

store/index.js

const cookieparser = process.server ? require('cookieparser') : undefined;
// 在服务端渲染期间运行都是同一个实例
// 为了防止数据冲突,务必要把 state 定义成一个函数,返回数据对象
export const state = () => {
  return {
    // 当前登录用户的登录状态
    user: null,
  };
};

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

export const actions = {
  // nuxtServerInit 是一个特殊的 action 方法
  // 这个 action 会在服务端渲染期间自动调用
  // 作用:初始化容器数据,传递数据给客户端使用
  nuxtServerInit({ commit }, { req }) {
    let user = null;
    // 如果请求头中有 Cookie
    if (req.headers.cookie) {
      // 使用 cookieparser 把 cookie 字符串转为 JavaScript 对象
      const parsed = cookieparser.parse(req.headers.cookie);
      try {
        user = JSON.parse(parsed.user);
      } catch (err) {
        // No valid cookie found
      }
    }
    // 提交 mutation 修改 state 状态
    commit('setUser', user);
  },
};

pages/login/index.vue

methods: {
  async onSubmit() {
    try {
      const { data } = this.isLogin
        ? await login({ user: this.user })
        : await register({ user: this.user });
      // 存储用户登录状态
      this.$store.commit('setUser', data.user);
      // 登录状态持久化
      Cookie.set('user', data.user);
      this.$router.push('/');
    } catch (error) {
      // console.dir(error);
      this.errors = error.response.data.errors;
    }
  },
},

处理导航栏链接展示状态

根据用户登录状态处理导航栏链接展示状态
在用户未登录的状态下,导航栏应该展示sign in sign up
用户登录的状态下,应该展示new post settings user info

pages/layout/index.vue

<template>
  <div>
    <!-- 顶部导航栏 -->
    <nav class="navbar navbar-light">
      <div class="container">
        <nuxt-link class="navbar-brand" to="/">conduit</nuxt-link>
        <ul class="nav navbar-nav pull-xs-right">
          <li class="nav-item">
            <!-- Add "active" class when you're on that page" -->
            <nuxt-link class="nav-link" exact to="/">Home</nuxt-link>
          </li>
          <template v-if="user">
            <li class="nav-item">
              <nuxt-link class="nav-link" to="/editor">
                <i class="ion-compose"></i>&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/1">
                <img class="user-pic" :src="user.image" />
                {{ user.username }}
              </nuxt-link>
            </li>
          </template>
          <template v-else>
            <li class="nav-item">
              <nuxt-link class="nav-link" to="/login">Sign in</nuxt-link>
            </li>
            <li class="nav-item">
              <nuxt-link class="nav-link" to="/register">Sign up</nuxt-link>
            </li>
          </template>
        </ul>
      </div>
    </nav>
    ...
  </div>
</template>

<script>
import { mapState } from 'vuex';
export default {
  name: 'Layout',
  computed: {
    ...mapState(['user']),
  },
};
</script>

<style lang="scss" scoped></style>

处理页面访问权限

在用户没有登录的情况下,我们应该是不允许用户访问
Nuxt 为我们提供了 路由中间件 的处理方案,这里的处理包含了服务端and 客户端的处理
未登录状态下需要处理的页面有: editor, profile, settings
登录状态下需要处理的页面有: sign up, sign in

  1. 创建文件 middleware/authenticated.js middleware/notAuthenticated.js
  2. 处理需要处理的文件:文件中添加相应的中间件即可

middleware/authenticated.js

export default function({ store, redirect }) {
  // If the user is not authenticated
  if (!store.state.user) {
    return redirect('/login');
  }
}

middleware/notAuthenticated.js

export default function({ store, redirect }) {
  // If the user is authenticated redirect to home page
  if (store.state.user) {
    return redirect('/');
  }
}

pages/editor/index.vue 【这里只列举一个使用,其他类似】

<script>
export default {
  name: 'Editor',
  middleware: 'authenticated',
};
</script>

首页

公共文章列表

1. 展示

  1. 封装数据接口
  2. 请求获取数据
  3. 绑定数据到视图

创建 api/article.js

import request from '@/utils/request';

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

pages/home/index.vue

<template>
  <div class="home-page">
    <div class="container page">
      <div class="row">
        <div class="col-md-9">
         ...
          <div
            class="article-preview"
            v-for="article in articles"
            :key="article.slug"
          >
            <div class="article-meta">
              <nuxt-link
                :to="{
                  name: 'profile',
                  params: {
                    username: article.author.username,
                  },
                }"
                ><img :src="article.author.image"
              /></nuxt-link>
              <div class="info">
                <nuxt-link
                  class="author"
                  :to="{
                    name: 'profile',
                    params: {
                      username: article.author.username,
                    },
                  }"
                  >{{ article.author.username }}</nuxt-link
                >
                <span class="date">{{ article.createdAt }}</span>
              </div>
              <button
                class="btn btn-outline-primary btn-sm pull-xs-right"
                :class="{
                  active: article.favorited,
                }"
              >
                <i class="ion-heart"></i> {{ article.favoritesCount }}
              </button>
            </div>
            <nuxt-link
              class="preview-link"
              :to="{
                name: 'article',
                params: {
                  slug: article.slug,
                },
              }"
            >
              <h1>
                {{ article.title }}
              </h1>
              <p>{{ article.description }}</p>
              <span>Read more...</span>
            </nuxt-link>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { getArticles } from '@/api/article';
export default {
  name: 'Home',
  async asyncData() {
    const { data } = await getArticles();
    return {
      articles: data.articles,
      articlesCount: data.articlesCount,
    };
  },
};
</script>

2. 分页处理
监听query 改变

  1. 分页参数处理
  2. 添加页码
  3. 将页码路由 query 处理

pages/home/index.vue

<template>
  <div class="home-page">
    ...
          <!-- 分页列表 -->
          <nav>
            <ul class="pagination">
              <li
                class="page-item"
                :class="{ active: item === page }"
                v-for="item in totalPage"
                :key="item"
              >
                <nuxt-link
                  class="page-link"
                  :to="{
                    name: 'home',
                    query: {
                      page: item,
                    },
                  }"
                  >{{ item }}</nuxt-link
                >
              </li>
            </ul>
          </nav>
          <!-- /分页列表 -->
        </div>
        ...
      </div>
    </div>
  </div>
</template>

<script>
import { getArticles } from '@/api/article';
export default {
  name: 'Home',
  watchQuery: ['page'],
  async asyncData({ query }) {
    const page = Number.parseInt(query.page || 1);
    const limit = 10;
    const { data } = await getArticles({
      limit,
      offset: (page - 1) * limit,
    });
    return {
      articles: data.articles,
      articlesCount: data.articlesCount,
      limit,
      page,
    };
  },
  computed: {
    totalPage() {
      return Math.ceil(this.articlesCount / this.limit);
    },
  },
};
</script>

文章标签列表

1. 展示
1. 封装列表接口
2. 请求接口数据
3. 绑定数据到视图
新建 api/tag.js

import request from '@/utils/request';

// 获取文章标签列表
export const getTags = () => {
  return request({
    method: 'GET',
    url: '/api/tags',
  });
};

pages/home/index.vue

<template>
  <div class="home-page">
   ...
        <div class="col-md-3">
          <div class="sidebar">
            <p>Popular Tags</p>

            <div class="tag-list">
              <nuxt-link
                :to="{
                  name: 'home',
                  query: {
                    tag: tag,
                  },
                }"
                class="tag-pill tag-default"
                v-for="tag in tags"
                :key="tag"
              >
                {{ tag }}
              </nuxt-link>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { getArticles } from '@/api/article';
import { getTags } from '@/api/tag';
export default {
  name: 'Home',
  watchQuery: ['page', 'tag'],
  async asyncData({ query }) {
    const page = Number.parseInt(query.page || 1);
    const limit = 10;
    const { data } = await getArticles({
      limit,
      offset: (page - 1) * limit,
      tag: query.tag,
    });
    const { data: tagData } = await getTags();
    return {
      articles: data.articles,
      articlesCount: data.articlesCount,
      limit,
      page,
      tags: tagData.tags,
    };
  }
};
</script>

2. 优化并行异步任务

<script>
export default {
  ...
  async asyncData({ query }) {
    const page = Number.parseInt(query.page || 1);
    const limit = 10;
    const [articleRes, tagsRes] = await Promise.all([
      getArticles({
        limit,
        offset: (page - 1) * limit,
        tag: query.tag,
      }),
      getTags(),
    ]);
    const { articles, articlesCount } = articleRes.data;
    const { tags } = tagsRes.data;
    return {
      articles,
      articlesCount,
      limit,
      page,
      tags,
    };
  },
  ...
};
</script>

3. 对应文章标签和文章列表

  1. 点击对应的Tag标签,将相应的tag 映射到连接中
  2. 页面监听到query中tag 的改变,请求接口返回数据更新视图
  3. 处理tag 列表下的page 改动的请求

导航栏部分

1. 展示状态处理

  • 关注的列表只能在登录状态下展示
  • 标签列表只能再点击了tag 之后才展示
  • 切换到关注的列表或者全部列表下不展示标签列表
  • 标签高亮部分处理
  • 导航部分处理:根据query 中的tab 来识别当前列表

pages/home/index.vue

<template>
  <div class="home-page">
    <div class="container page">
      <div class="row">
        <div class="col-md-9">
          <div class="feed-toggle">
            <ul class="nav nav-pills outline-active">
              <li class="nav-item" v-if="user">
                <nuxt-link
                  class="nav-link"
                  :class="{
                    active: tab === 'your_feed',
                  }"
                  :to="{
                    name: 'home',
                    query: {
                      tab: 'your_feed',
                    },
                  }"
                  exact
                  >Your Feed</nuxt-link
                >
              </li>
              <li class="nav-item">
                <nuxt-link
                  class="nav-link"
                  :class="{
                    active: tab === 'global_feed',
                  }"
                  :to="{
                    name: 'home',
                    query: {
                      tab: 'global_feed',
                    },
                  }"
                  exact
                  >Global Feed</nuxt-link
                >
              </li>
              <li class="nav-item" v-if="tag">
                <nuxt-link
                  class="nav-link"
                  :class="{
                    active: tab === 'tag',
                  }"
                  :to="{
                    name: 'home',
                    query: {
                      tag,
                      tab: 'tag',
                    },
                  }"
                  >#{{ tag }}</nuxt-link
                >
              </li>
            </ul>
          </div>
          ...
        </div>

        <div class="col-md-3">
          <div class="sidebar">
            <p>Popular Tags</p>

            <div class="tag-list">
              <nuxt-link
                :to="{
                  name: 'home',
                  query: {
                    tag,
                    tab: 'tag',
                  },
                }"
                class="tag-pill tag-default"
                v-for="tag in tags"
                :key="tag"
              >
                {{ tag }}
              </nuxt-link>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { getArticles } from '@/api/article';
import { getTags } from '@/api/tag';
import { mapState } from 'vuex';
export default {
  name: 'Home',
  watchQuery: ['page', 'tag', 'tab'],
  async asyncData({ query }) {
    const page = Number.parseInt(query.page || 1);
    const limit = 10;
    const [articleRes, tagsRes] = await Promise.all([
      getArticles({
        limit,
        offset: (page - 1) * limit,
        tag: query.tag,
      }),
      getTags(),
    ]);
    const { articles, articlesCount } = articleRes.data;
    const { tags } = tagsRes.data;
    return {
      articles,
      articlesCount,
      limit,
      page,
      tags,
      tab: query.tab || 'global_feed',
      tag: query.tag,
    };
  },
  computed: {
    ...mapState(['user']),
    totalPage() {
      return Math.ceil(this.articlesCount / this.limit);
    },
  },
};
</script>

2. 对应数据与状态的对应关系

  • 在切换页码的时候处理一下query 值
  • 处理 关注的列表下的标签列表和全部列表下的标签列表
    ! 这里注意一下,关注的列表需要Token 才能正常访问,否则会爆出401错误,我们这里暂时写死token 处理,后续集中处理该问题

api/article.js

// 获取关注的文章列表
export const getFeedArticles = (params) =>
  request({
    method: 'GET',
    url: '/api/articles/feed',
    params,
    headers: {
      Authorization: `Token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTUzMzY2LCJ1c2VybmFtZSI6IuiNieiOkyIsImV4cCI6MTYyMjEwMDU3OH0.yUfoKBjRli1pf5WVWDHgTd19tJUDESYqgh6R_6RDDPs`,
    },
  });

pages/home/index.vue

<script>
import { getArticles, getFeedArticles } from '@/api/article';
import { getTags } from '@/api/tag';
import { mapState } from 'vuex';
export default {
  name: 'Home',
  watchQuery: ['page', 'tag', 'tab'],
  async asyncData({ query }) {
    const page = Number.parseInt(query.page || 1);
    const limit = 2;
    const { tab = 'global_feed', tag } = query;
    const loadArticles = tab === 'global_feed' ? getArticles : getFeedArticles;
    const [articleRes, tagsRes] = await Promise.all([
      loadArticles({
        limit,
        offset: (page - 1) * limit,
        tag,
      }),
      getTags(),
    ]);
    const { articles, articlesCount } = articleRes.data;
    const { tags } = tagsRes.data;
    return {
      articles,
      articlesCount,
      limit,
      page,
      tags,
      tab,
      tag,
    };
  }
};
</script>

设置用户Token

前置知识

  1. Nuxt 插件
  2. axios拦截器

由于我们需要在拦截器中访问当前用户的信息,所以我们需要使用Nuxt 插件进行处理这件事

  • 遵循Nuxt标准,我们需要将util 中的封装,转移到plugins 中[修改util 文件夹名字为plugins]
  • 在 nuxt.config.js 中注册插件
  • 处理request.js 中的拦截器内容[见官方使用]
  • 修改request调用处为当前调用

plugins/request.js

import axios from 'axios';
// https://conduit.productionready.io
export const request = axios.create({
  baseURL: 'https://conduit.productionready.io',
});

// 通过插件机制获取到上下文对象(query、params、req、res、app、store...)
// 插件导出函数必须作为 default 成员
export default ({ store }) => {
  // 请求拦截器
  // Add a request interceptor
  // 任何请求都要经过请求拦截器
  // 我们可以在请求拦截器中做一些公共的业务处理,例如统一设置 token
  request.interceptors.request.use(
    function(config) {
      // Do something before request is sent
      // 请求就会经过这里
      const { user } = store.state;
      if (user && user.token) {
        config.headers.Authorization = `Token ${user.token}`;
      }

      // 返回 config 请求配置对象
      return config;
    },
    function(error) {
      // 如果请求失败(此时请求还没有发出去)就会进入这里
      // Do something with request error
      return Promise.reject(error);
    }
  );
};

发布时间格式化

我们这里使用一个插件处理
考虑到我们也会在文章中使用格式化处理时间,所以我们这里同样注册一个插件

  1. 新建文件 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);
    });
    
  2. 注册插件

  3. 使用插件

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

文章点赞

功能描述:
点击favoriteIcon 可添加点赞,数量加一,favorite点亮,再次点击,取消点赞

  1. 封装接口【添加赞,删除赞
  2. 注册事件
  3. 处理用户多次点击事件
methods: {
  async onFavorite(article) {
    article.favoriteDisabled = true;
    if (article.favorited) {
      // 取消赞
      await onCancelFavorite(article.slug);
      article.favorited = false;
      article.favoritesCount -= 1;
    } else {
      // 赞
      await onFavorite(article.slug);
      article.favorited = true;
      article.favoritesCount += 1;
    }
    article.favoriteDisabled = false;
  },
},

文章详情页

1. 数据动态展示

  1. 接口封装
  2. 将请求数据绑定到页面视图
  3. 解析文章内容markdown为html 格式【使用markdown-it插件】
  4. 封装文章作者信息使用

2. 设置页面meta
3. 文章评论

可以提交、删除评论

  1. 将评论部分抽象为组件展示
  2. 封装接口
  3. 将数据绑定到视图展示

发布部署

  1. 打包 nuxt应用
    1. 添加相应的指令到package.json
    2. 执行打包命令
    3. 使用start 验证打包结果
    

自动部署

使用GitHub Actions 实现自动部署

1. CI/CD 服务

目的: 实现持续集成/部署

  • Jenkins
  • Gitlab CI
  • GitHub Actions
  • Travis CI
  • Circle CI

2. 通过 GitHub Actions 实现自动部署

环境准备

  • linux 服务器
  • 把代码提交到GitHub 远程仓库

配置GitHub Access Token

作用:在CI/CD 中使用Github用户的身份令牌来访问/操作Github仓库

  • 生成 https://github.com/settings/tokens
  • 配置到项目的Secrets 中 https://github.com/[your github name]/[your github project]/settings/secrets/actions

配置 GitHub Actions 执行脚本

  • 在项目根目录创建 .github/workflows 目录
  • 下载 main.yml 到 workflow 目录中
  • 修改配置
  • 配置PM2 配置文件(根目录下创建pm2.config.json)
  • 提交更新
  • 查看自动部署状态
  • 访问网站
  • 提交更新…

下载main.yml 之后修改的部分
在这里插入图片描述在这里插入图片描述
创建secrets 需要的 secrets.HOST…

推送代码到远程仓库

项目打版 git tag v***

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值