Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

一步步搭完整NuxtJS项目 #41

Open
janeLLLL opened this issue Dec 22, 2020 · 0 comments
Open

一步步搭完整NuxtJS项目 #41

janeLLLL opened this issue Dec 22, 2020 · 0 comments
Labels

Comments

@janeLLLL
Copy link
Owner

janeLLLL commented Dec 22, 2020

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

接口文档:https://github.com/gothinkster/realworld/tree/master/api

页面模板:https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md

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

代码仓库:https://github.com/janeLLLL/realworld-nuxtjs

在线预览: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. 启动服务

导入样式资源

对照https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md一个个页面导入即可

格式:[页面模板名称] === [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自动化部署

https://frostming.com/2020/04-26/github-actions-deploy/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant