【项目记录】Vue基于NUXT的SSR应用

1,279 阅读4分钟

本篇文章主要记录项目开发过程及nuxt的基本使用

新建项目

首先在github上新建一个repository,下载到本地,用编辑器打开

利用nuxt提供的脚手初始化项目
vue init nuxt/starter

安装依赖
npm install

执行
npm run dev

目录结构

nuxt初始化的项目结构为

每个文件目录,官网均有介绍,这里主要介绍以下几个目录

一、nuxt.config.js

用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置

比如配置header,全局css,plugin等,具体使用看下文

二、layouts

nuxt新增了layouts概念,初始化layouts/index.vue,<nuxt /> 组件用于显示页面的主体内容,本项目只用了index.vue,后期可以考虑优化

<template>
  <div>
    <nuxt />
  </div>
</template>

nuxt允许在 layout 目录下创建自定义的布局,从而实现页面的多个布局切换,将页面分为三层layouts => page => component,可以减少代码冗余

1、在layouts下新建[name].vue布局,其中default.vue 文件来扩展应用的默认布局,error.vue 文件来定制化错误页面,错误页面仅是page,不需要包含 <nuxt/> 标签

2、在page通过layout属性访问自定义的布局,如果不写,则默认是默认布局

export default {
  layout: 'name'
}

三、plugins

用于组织那些需要在根vue.js应用实例化之前需要运行的 Javascript 插件。

比如本项目使用到的

1. echarts

安装echarts

npm install echarts -S

在plugin目录下新建echarts.js

import Vue from 'vue'
import echarts from 'echarts' // 引入echarts
Vue.prototype.$echarts = echarts 

在nuxt.config.js 配置

 plugins: ['~plugins/echarts']

2. vue-awesome-swiper

安装vue-awesome-swiper

npm install vue-awesome-swiper -S

在plugins目录下新建vueSwiper.js

import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr'

Vue.use(VueAwesomeSwiper)

在nuxt.config.js配置全局css等

  css: [
    "swiper/dist/css/swiper.css"
  ],
  
  plugins: [{
    src: '~plugins/vueSwiper',
    ssr: false
   }
  ],

四、store

用于组织应用的 Vuex 状态树 文件

如本项目的nav需要用到store 在store新建nav.js

export const state = () => ({
  activeUrl: '/'
});
const getters = {
  getActiveUrl: state => state.activeUrl
};
const mutations = {
  setActiveUrl(state, num) {
    state.activeUrl = num;
  }
};
/*const actions = {
async SET_isLogin({state, commit}, val) {
commit('SET_isLogin', val);
}
};*/

export default {
  namespaced: true,
  state,
  getters,
  // actions,
  mutations
};

配置路由

Nuxt.js 依据 pages 目录结构自动生成 vue-router 模块的路由配置

一、基础路由

无需手动配置,可在.nuxt/router.js下看到具体的路由

如本项目的pages目录结构

Nuxt.js自动生成的路由配置如下:

二、<nuxt-link>

在页面使用路由用<nuxt-link>标签

  <ul class="nav-list">
      <li
        v-for="(item, index) in list"
        v-bind:key="index"
        class="nav-item"
        @click="navigater(item.url)"
      >
        <nuxt-link :to="item.url" :class="[item.url == activeUrl ? 'active' : '']">
          {{
          item.title
          }}
        </nuxt-link>
      </li>
    </ul>

三、动态路由

在 Nuxt.js 里面定义带参数的动态路由,只需创建对应的以下划线作为前缀的 Vue 文件或目录即可

比如,在pages目录下新建user文件夹,user下新建_id.vue,则nuxt会生成

 {
    name: 'users-id',
    path: '/users/:id?',
    component: 'pages/users/_id.vue'
 },

此外,nuxt还支持在动态路由组件中定义参数校验方法

比如在 pages/users/_id.vue

export default {
  validate ({ params }) {
    // 必须是number类型
    return /^\d+$/.test(params.id)
  }
}

如果校验方法返回的值不为 true或Promise中resolve 解析为false或抛出Error , Nuxt.js 将自动加载显示 404 错误页面或 500 错误页面

api实现

服务端

nuxt的server端使用的是express,数据库使用的是mysql,server端目录组织如图:

  • db.js是连接数据库的配置

  • sql.js是sql语句

  • index.js是express的启动文件

const express = require('express')
const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')
const app = express()
const router = require('./router');
const path = require('path');

// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = process.env.NODE_ENV !== 'production'

async function start() {
  // Init Nuxt.js
  const nuxt = new Nuxt(config)

  const { host, port } = nuxt.options.server

  // Build only in dev mode
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  } else {
    await nuxt.ready()
  }
  
  app.use(router)
  app.use(nuxt.render)


  // Listen the server
  app.listen(port, host)
  consola.ready({
    message: `Server listening on http://${host}:${port}`,
    badge: true
  })
}
start()

api.js

const mysql = require('mysql');
const db = require('./db');
const sql = require('./sql');

/**
 * 查询导航栏数据
 */
exports.queryNav = () => {
    return new Promise((resolve, reject) => {
        const con = mysql.createConnection(db);
        con.connect();
        con.query(sql.queryNav, (err, result) => {
            if (err) {
                reject('err:queryNav');
            }
            resolve(result);
        });
        con.end();
    })
}

router.js

const express = require('express');
const router = express.Router();
const api = require('./api');

router.get('/api/nav', async (req, res, next) => {
    let data = await api.queryNav();
    res.send(data);
    res.end();
})

前端

在根目录下新建data文件夹

httpUtil.js是对用来封装axios

import axios from 'axios';
import qs from 'qs';

axios.defaults.timeout = 5000;                        //响应时间
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';        //配置请求头
// axios.defaults.baseURL = 'http://localhost:3000';   //配置接口地址

let baseUrl="http://localhost:3000/api";
// let baseUrl="https://www.xxx";

//POST传参序列化(添加请求拦截器)
axios.interceptors.request.use((config) => {
    //在发送请求之前做某件事
    if(config.method  === 'post'){
        config.data = qs.stringify(config.data);
    }
    return config;
},(error) =>{
    // console.log('错误的传参');
    return Promise.reject(error);
});

//返回状态判断(添加响应拦截器)
axios.interceptors.response.use((res) =>{
    //对响应数据做些事
    if(!res.data.success){
        return Promise.resolve(res);
    }
    return res;
}, (error) => {
    return Promise.reject(error);
});

//返回一个Promise(发送post请求)
export function fetchPost(url, params) {
    return new Promise((resolve, reject) => {
        axios.post(baseUrl+url, params)
            .then(response => {
                resolve(response.data);
            }, err => {
                reject(err);
            })
            .catch((error) => {
                reject(error)
            })
    })
}
//返回一个Promise(发送get请求)
export function fetchGet(url) {
    return new Promise((resolve, reject) => {
        axios.get(baseUrl+url)
            .then(response => {
                resolve(response.data)
            }, err => {
                reject(err)
            })
            .catch((error) => {
                reject(error)
            })
    })
}
export default {
    fetchPost,
    fetchGet,
}

api.js 用来统一管理接口

import { fetchGet, fetchPost } from "./httpUtil";

/**
 * 获取Nav内容
 */
export const getQueryNav = () => {
  return fetchGet("/nav");
};

export default {
    getQueryNav
}

视图层使用

import Api from "~/data/api.js";
export default {
  async asyncData() {
    const studentList = await Api.getStudentList();
    return {
      studentList
    };
  }
};

asyncData异步数据

Nuxt.js 扩展了 Vue.js,增加了一个叫 asyncData 的方法,使得我们可以在设置组件的数据之前能异步获取或处理数据。

asyncData方法会在组件(限于页面组件)每次加载之前被调用。它可以在服务端或路由更新之前被调用,该方法第一个参数被设定为当前页面的上下文对象,可以利用 asyncData方法来获取数据,Nuxt.js 会将 asyncData 返回的数据融合组件data 方法返回的数据一并返回给当前组件。

由于asyncData方法是在组件初始化前被调用的,所以在方法内是没有办法通过this 来引用组件的实例对象

比如本项目中有需要获取router中的query

async asyncData({query}) {
    const detail = await Api.getStudentDetail(query.id);
    return {
      detail
    };
  },

优化

图片懒加载

安装vue-lazyload:

npm install vue-lazyload -S

在plugins目录下新建vue-lazyload.js:

import Vue from 'vue'
import VueLazyload from 'vue-lazyload'

Vue.use(VueLazyload);

nuxt.config.js配置

 plugins: ['~plugins/echarts', {
    src: '~plugins/vueSwiper',
    ssr: false
  }, {
    src: '~/plugins/vue-lazyload',
    ssr: false
  },
  ],
 build: {
    extend(config, ctx) {
    },
    vendor:['vue-lazyload'],
 }

在template img 标签将src换为

<img v-lazy="require('../../assets/img/bg.png')" alt />
使用变量
<img v-lazy="imgUrl" alt />