一步步搭完整NuxtJS项目

505 阅读2分钟

本文首发于我的GitHub博客,其它平台同步更新。


本文以realworld为学习模板,重现示例的网站

接口文档:github.com/gothinkster…

页面模板:github.com/gothinkster…

本文旨在一步步搭建起完整的NuxtJS项目,方便学习

代码仓库:github.com/janeLLLL/re…

在线预览:http://182.92.210.40:3000

项目结构

realworld-nuxtjs
 ├── .git
 ├── .gitignore
 ├── .nuxt
 ├── .vscode
 ├── api
 ├── app.html
 ├── LICENSE
 ├── middleware
 ├── node_modules
 ├── nuxt.config.js
 ├── package-lock.json
 ├── package.json
 ├── pages
 ├── plugins
 ├── pm2.config.js
 ├── README.md
 ├── realworld-nuxtjs.zip
 ├── static
 └── store

创建项目

  1. 生成 package.json 文件 / 安装 nuxt 依赖
# 创建项目目录
mkdir realworld-nuxtjs
# 进入项目目录
cd realworld-nuxtjs
# 生成 package.json 文件
npm init -y
# 安装 nuxt 依赖
npm install nuxt
  1. package.json中添加启动脚本
"scripts": {
	"dev": "nuxt"
}
  1. 创建 pages/index.vue
  2. 启动服务

导入样式资源

对照github.com/gothinkster…

格式:[页面模板名称] === [Nuxtjs视图]

Header === 模板

app.html

<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head {{ HEAD_ATTRS }}>
    {{ HEAD }}
      <link href="//code.ionicframework.com/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">
      
  </head>
  <body {{ BODY_ATTRS }}>
    <h1>app.html</h1>
    {{ APP }}
  </body>
</html>

Header === 视图布局

pages/layout/index.vue

  • 需要区分登录与未登录状态
<template>
  <!-- 顶部导航栏 -->
  <div>
    <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">
            <nuxt-link class="nav-link" to="/" exact>Home</nuxt-link>
          </li>
          <!-- 登录了 -->
          <template v-if="user">
            <!--粘贴模板-->
          </template>
          <!-- 未登录 -->
          <template v-else>
            
          </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>

登录注册

pages/login.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="/login">Have an account?</nuxt-link>
            <nuxt-link v-else to="/register">Need an account?</nuxt-link>
          </p>

          <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>

          <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"
                minlength="8"
                required
              />
            </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 { login, register } from "@/api/user";

export default {
  middleware: 'notAuthenticated',
  name: "LoginIndex",
  computed: {
    isLogin() {
      return this.$route.name === "login";
    },
  },
  data() {
    return {
      user: {
        username: "",
        email: "2676697536@qq.com",
        password: "",
      },
      errors: [], //错误信息
    };
  },
  methods: {
    async onSubmit() {
        //...
    },
  },
};
</script>

<style>
</style>

剩余页面(略)

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/')
                        },
                        {
                            path: '/login',
                            name: 'login',
                            component: resolve(__dirname, 'pages/login/')
                        },
                        {
                            path: '/register',
                            name: 'register',
                            component: resolve(__dirname, 'pages/login/')
                        },
                        {
                            path: '/profile',
                            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/')
                        },
                    ]
                }
            ])
        }
    },
}

封装请求模块

安装axios:

npm i axios

plugins\request.js

import axios from 'axios'
const request = axios.create({
baseURL: 'https://conduit.productionready.io/'
})
export default request
  • api\user.js
import { request } from '@/plugins/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
    })
}

//修改
export const updateUser = data => {
    return request({
        method: 'PUT',
        url: "/api/user",
        data
    })
}

pages\login\index.vue引入:

import { login, register } from "@/api/user";
  • api\tag.js
  • api\profile.js
  • api\article.js

登录注册

  • 封装请求
  • 表单验证
  • 错误处理
  • 用户注册

跨域身份验证

登录态持久化

// 仅在客户端加载 js-cookie 包
const Cookie = process.client ? require('js-cookie') : undefined

async onSubmit() {
      try {
        //TODO: 保存用户的登录状态
        //程序在运行期间存储到内存中,方便共享,刷新就没了
        this.$store.commit("setUser", data.user);
        //为了防止刷新特面数据丢失,我们要把数据持久化
        Cookie.set('user',data.user)
      } catch (error) {
          ...
      }
    },

存储用户登录状态 === Vuex

store

index.js 在此目录中创建 文件将启用存储

  1. 导入Vuex
  2. store选项添加到根Vue实例


  • 使用Vuex
  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: {}
})
}
  1. 登录成功,将用户信息存入容器
this.$store.commit('setUser', data.user)
  1. 将登录状态持久化到 Cookie 中

安装js-cookie:

npm i js-cookie
const Cookie = process.client ? require('js-cookie') : undefined
Cookie.set('user', data.user)
  1. 从 Cookie 中获取并初始化用户登录状态

安装cookieparser:

npm i cookieparser

store/index.js

const cookieparser = process.server ? require('cookieparser') : undefined

actions: {
	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)
    }
}

处理页面访问权限 === 路由中间件

middleware\authenticated.js:没登录转跳登录页

/**
 * 验证是否登录的中间件
 */

export default function ({ store, redirect }) {
    // If the user is not authenticated
    console.log('store',store.state)
    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('/')
    }
  }

在需要判断登录权限的页面中配置使用中间件:

middleware: 'notAuthenticated',
middleware: "authenticated",

首页模块

  • pages\home\index.vue

获取数据

async asyncData () {
    const { data } = await getArticles()
    return data
}

分页处理

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
    }
},

页码处理

  1. 使用计算属性计算总页码
totalPage () {
	return Math.ceil(this.articlesCount / this.limit)
}
  1. 遍历生成页码列表
<nav v-if="$route.query.tab !== 'your_feed'">
            <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>
  1. 设置导航链接
  2. 响应 query 参数的变化
watchQuery: ['page'],

视图处理

  • 没有登录不展示 my-feed
  • 处理 tab 切换(query)以及高亮(exact)问题
<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>

统一添加数据TOKEN

import axios from 'axios'

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

nuxtServerInit

展示文章标签列表

  1. 封装接口请求方法

  2. 数据过滤

    • 标签链接
    <li v-if="tag" class="nav-item">
                    <nuxt-link
                      class="nav-link active"
                      :class="{ active: tab === 'tag' }"
                      :to="{
                        name: 'home',
                        query: {
                          tab: 'tag',
                          tag: tag,
                        },
                      }"
                      >#{{ tag }}</nuxt-link
                    >
                  </li>
    
    • 分页页码链接
    <nav v-if="$route.query.tab !== 'your_feed'">
                <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>
    

日期格式处理

npm install dayjs --save

引入nuxtjs的插件机制:nuxt.config.js

//注册插件
    plugins: [
        '~/plugins/dayjs.js'
    ]

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

部署

服务器安装node.js和npm

  1. 配置Host + Port

    nuxt.config.hs

module.exports = {
    server: {
        host: '0.0.0.0',//监听所有网卡地址
        port: 3000
    }
}
  1. 压缩发布包

  1. 把发布包传到服务端
#连接服务器
ssh root@182.92.21
#上传压缩包
scp ./realworld-nuxtjs.zip root@182.92.210.40:/root/realworld-nuxtjs
  1. 解压
unzip ./realworld-nuxtjs.zip
  1. 安装依赖
npm i
  1. 启动服务器
npm run start

使用PM2启动服务

pm2.config.js

{
    "apps": "RealWorld",
    "script": "npm",
    "args": "start"
}

服务器上运行:

#全局安装PM2
npm install --global pm2
#建立软连接
ln -s /root/node/node-v14.15.3-linux-x64/bin/pm2 /usr/local/bin/pm2
#运行脚本文件
pm2 start npm -- start

github自动化部署

frostming.com/2020/04-26/…