# Nuxt基础知识

Nuxt是一个Vue版本的SSR服务端渲染框架,一般SPA单页面应用在写门户网站、博客、新闻等网站时SEO都不是很友好,不利于搜索引擎优化,以前可以使用预渲染插件进行SEO优化,但效果不是很好,而Nuxt就可以很好的帮我们解决这个问题,不仅能使用Vue单页面进行开发,打包还能生成对应的多页面,使网页源码呈现出来。

项目构建模板:nuxt-app模板 (opens new window)

# 一、构建项目

Nuxt为我们提供了两种模式,一种是SPA单页面纯静态模式,另一种是Universal服务端渲染模式,我们使用Nuxt就是为了服务端渲染,所以就不需要使用SPA这种模式了。在使用Universal服务端渲染模式时,有两种打包方式供我们选择static和server,static是静态预渲染,此方式打包好后就是一个预渲染好的纯静态页面,不需要启动后端服务,直接部署即可,和Vue的预渲染插件同等效果,我们如果只是开发简单官网和一些新闻页使用此模式就满足了。server打包方式就是偏向服务端渲染,打包后是包含后端服务文件的,主要是Node.js,部署时需要启动服务才行,此方式是真正的服务端渲染。

npx create-nuxt-app <项目名>

执行后,根据提示进行选择,渲染模式Rendering mode选择Universal (SSR / SSG),打包方式Deployment target选择Static,创建好后 npm run dev 运行项目就行了。

? Project name: nuxt-app
? Programming language: JavaScript 
? Package manager: Npm
? UI framework: Ant Design Vue     
? Template engine: HTML
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: ESLint, Prettier    
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: None

# 二、使用方式

Nuxt的代码基础书写方式还是和Vue一样,但是有的为了服务端渲染使用方式还是有所区别。

# 1、声明周期

Vue 组件的生命周期都可以使用, 但只有 beforeCreate 和 created 这两个方法会在 客户端和服务端被调用。其他生命周期函数仅在客户端被调用。所以获取数据一般要在beforeCreate 和 created中获取。

# 2、路由

获取当前路由和路由元和Vue一样,nuxt没有路由配置,路由是根据pages里创建的文件夹和文件自动生成的。

<template>
  <nuxt-link to="/">首页</nuxt-link>
</template>

<script>
export default {
  created() {
    console.log(this.$route)
    console.log(this.$route.params.id)
    console.log(this.$router)
  }
  methods: {
    // 动态路由
    gotoPage(id) {
      this.$router.push({
        path: `/news/${id}`
      })
    }
  }
}
</script>

nuxt路由使用方式:

<nuxt-link to=/">首页</nuxt-link> <router-link to="/">首页</router-link>
<nuxt /> <router-view></router-view>
<nuxt-child> 显示嵌套路由页面内容

# 3、less/sass引入

安装时一定要注意版本,默认安装的版本都会过高,需要降版处理。

npm i less less-loader -D
npm install node-sass sass-loader -D

Nuxt没有main.js文件,全局css或者插件的样式都要在nuxt.config的css配置进行引入。

export default {
  target: 'static',

  // 引入全局CSS
  css: [
    // 'ant-design-vue/dist/antd.css', 
    '@/assets/less/main.less',
    '@/assets/less/theme.less'
    // 'ant-design-vue/dist/antd.less'
  ],



  // less、sass配置
  build: {
    loaders: {
      less: {
        lessOptions: { // less-loader 5.x以上才有 lessOptions , 5.x 以下直接配置
          javascriptEnabled: true
        }
      }
    }
  }
}

全局变量配置: 安装@nuxtjs/style-resources模块,然后引入进行配置。

npm i @nuxtjs/style-resources -D
export default {
  target: 'static',

  // 引入全局CSS
  css: [
    // 'ant-design-vue/dist/antd.css', 
    '@/assets/less/main.less',
    '@/assets/less/theme.less'
    // 'ant-design-vue/dist/antd.less'
  ],
  
  modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios', // nuxt自带的axios请求模块
    '@nuxtjs/proxy',
    '@nuxtjs/style-resources'
  ],

  // less变量全局配置
  styleResources: {
    less: ['@/assets/less/_flex.less', '@/assets/less/_common.less', '@/assets/less/theme.less']
  },
  // less、sass配置
  build: {
    loaders: {
      less: {
        lessOptions: { // less-loader 5.x以上才有 lessOptions , 5.x 以下直接配置
          javascriptEnabled: true
        }
      }
    }
  }
}

# 4、静态文件使用

static里的文件不会被打包,相当于在根路径下,引入直接使用 / 路径,assets文件会被打包,使用相对路径来引入。

// assets中图片引入 
<img src="@/assets/image.png" />

// static中图片引入 
<img src="/image.png" />

# 5、引入插件

插件安装后,引入需要在plugins文件下新建对应的js文件,在该文件中和在main.js文件中引入一样引入,然后把该js文件在nuxt.config的plugins中引入即可生效。

1、引入Ant Design Vue

npm i ant-design-vue

在plugins下创建一个js文件,引入Vue和插件,注册插件.

// plugins/antd.js
import Vue from 'vue'
import Antd from 'ant-design-vue/lib'

Vue.use(Antd)

然后把整个文件在配置中引入,其他插件也是如此使用,比在Vue中使用多了一个步骤,基本还是一样。

export default {
  target: 'static',
  // 插件引入
  plugins: ['@/plugins/antd'],
}

2、引入swiper

npm i vue-awesome-swiper@3.1.3

在plugins下创建一个js文件,引入Vue和插件,注册插件.

// plugins/swiper.js
import Vue from 'vue'
import 'swiper/dist/css/swiper.css'
// import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr'

// swiper有Window对象,在服务端无法渲染
if (process.client) {
  const VueAwesomeSwiper = require('vue-awesome-swiper/dist/ssr')
  Vue.use(VueAwesomeSwiper)
}

在plugins中引入

export default {
  target: 'static',
  // 插件引入
  plugins: ['@/plugins/swiper'],
}

使用

<template>
  <div class="index">
    <div class="index-banner">
      <div ref="mySwiper" v-swiper:mySwiper="swiperOption">
        <div class="swiper-wrapper">
          <div class="swiper-slide">
            <img src="@/assets/images/banner01.jpg" alt="">
          </div>
          <div class="swiper-slide">
            <img src="@/assets/images/banner02.jpg" alt="">
          </div>
          <div class="swiper-slide">
            <img src="@/assets/images/banner03.jpg" alt="">
          </div>
          <div class="swiper-slide">
            <img src="@/assets/images/banner04.jpg" alt="">
          </div>
        </div>
        <div class="swiper-pagination"></div>
      </div>
    </div>
  </div>
</template>

<script>
/**
 * @desc 新闻页
 * @author changz
 * */

export default {
  name: 'News',
  // 当前页面使用的基础布局
  layout: 'BasicLayout',
  data() {
    return {
      title: '新闻页',
      swiperOption: {
        loop: true,
        autoplay: {
          delay: 3000
        },
        pagination: {
          el: '.swiper-pagination'
        },
        observer: true,
        observeParents: true
      }
    }
  },
  computed: {
    swiperData() {
      return this.$refs.mySwiper.swiper
    }
  },
  methods: {
  }
}
</script>

<style lang="less" scoped>
.index {
  width: 100%;
  .index-banner {
    width: 100%;
    height: 400px;
    background-color: #fff;
    .swiper-wrapper {
      width: 100%;
      height: 100%;
      .swiper-slide {
        width: 100%;
        height: 100%;
        img {
          width: 100%;
          height: 100%;
        }
      }
    }
  }
}
</style>

# 6、模块

Nuxt专门提供了模块功能来扩展其功能,Nuxt默认提供了很多模块,如axios请求模块,proxy跨域模块等,也可以到其社区去搜索相关模块https://github.com/topics/nuxt-module (opens new window)

安装模块:

npm install @nuxtjs/axios

在modules中引入

export default {
  // 打包模式 static 静态预渲染 server 服务端渲染
  target: 'static',
  // nuxt内置模块
  modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios', // nuxt自带的axios请求模块
    '@nuxtjs/proxy'
  ],

  // Axios module configuration: https://go.nuxtjs.dev/config-axios
  axios: {
    // Workaround to avoid enforcing hard-coded localhost:3000: https://github.com/nuxt-community/axios-module/issues/308
    // baseURL: '/',
    proxy: true
  },

  // 跨域设置
  proxy: {
    '/api': {
      target: 'https://www.ahss.com.cn', // 目标接口域名
      // pathRewrite: {
      //   '^/api': '/', // 把 /api 替换成 /
      // }
    }
  }
}

# 7、布局layout

项目中页面的公共部分,如头部、底部、或者公共模板,可以像嵌套路由那样进行渲染公共布局页面,和后台管理系统的layout布局一样。

<template>
  <div class="basic">
    <header class="basic-header">
    </header>
    <main class="basic-main">
      <nuxt />
    </main>
    <footer class="basic-footer">
    </footer>
  </div>
</template>

<script>
/**
 * @description 公共布局页面
 * @author changz
 * */
export default {
  name: 'BasicLayout',
  data() {
    return {
      pageLoad: false
    }
  },
  created() {
    console.log(this.$route)
    // this.routeList
  }
}
</script>

可通过添加 layouts/BasicLayout.vue 文件来扩展应用的基础布局,通过在布局文件中添加  组件来显示页面的主体内容,然后在页面中通过layout引入该布局使用。

<template>
  <!-- Your template -->
</template>
<script>
  export default {
    layout: 'BasicLayout'
    // page component definitions
  }
</script>

# 8、接口数据请求

接口请求可以直接使用axios也可以使用Nuxt自带的的模块,两者都可以使用,如果使用axios实例请求,一定要创建好实例后再调用,否则页面多次刷新会报错。为了防止出现这种报错,我们直接使用Nuxt提供的模块@nuxtjs/axios开请求数据。

数据请求分为两种情况,如果只是请求数据,不对数据做SEO优化,那么和正常的接口请求一样,直接使用就可以了,如果要是想对数据做预渲染则需要把请求放在asyncData方法中,asyncData中不能使用this,也不能直接赋值data的数据,需要通过return返回数据,返回的数据和data一样,可以直接使用。

安装axios,然后在nuxt.config的模块中引入。

npm install @nuxtjs/axios

在nuxt.config中的axios中设置,也可以创建一个插件,在plugins引入设置进行全局请求拦截。

export default {
  // 打包模式 static 静态预渲染 server 服务端渲染
  target: 'static',
  
  // 引入插件
  plugins: [
    '@/plugins/axios'
  ],
  // nuxt内置模块
  modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios', // nuxt自带的axios请求模块
    '@nuxtjs/proxy'
  ],

  // Axios module configuration: https://go.nuxtjs.dev/config-axios
  axios: {
    // Workaround to avoid enforcing hard-coded localhost:3000: https://github.com/nuxt-community/axios-module/issues/308
    baseURL: '/',
    proxy: true
  },

  // 跨域设置
  proxy: {
    '/api': {
      target: 'https://www.ahss.com.cn', // 目标接口域名
      // pathRewrite: {
      //   '^/api': '/', // 把 /api 替换成 /
      // }
    }
  }
}

全局请求拦截

// plugins/axios.js
/**
 * @desc 全局请求拦截
 * */ 
import storage from 'store'
import notification from 'ant-design-vue/lib/notification'

export default function ({ $axios, redirect }) {
  $axios.onRequest(config => {
    const token = storage.get('ACCESS_TOKEN')
    // 如果 token 存在
    // 让每个请求携带自定义 token 请根据实际情况自行修改
    if (token) {
      config.headers['Authorization'] = token
    }
  })

  $axios.onError(error => {
    const code = parseInt(error.response && error.response.status)
    if (code === 401) {
      storage.clearAll()
      notification.warning({
        message: '提示',
        description: '授权验证失败,请重新登录'
      })
      setTimeout(() => {
        // window.location.reload()
        redirect('/empower')
      }, 1000)
      return new Promise(() => {})
    } else if (code !== 200) {
      notification.warning({
        message: '提示',
        description: error.message
      })
      return new Promise(() => {})
    }
  })
}

在页面中使用

export default {
  asyncData ({ $axios, params }) {
    return $axios.get(`https://my-api/posts/${params.id}`)
      .then((res) => {
        return { title: res.data.title }
      })
  },

  created() {
    this.getVerifyCode()
  },
  methods: {
    // 获取验证码
    getVerifyCode() {
      this.codeLoad = true
      this.$axios.$get('/api/captchas').then(res => {
        this.codeLoad = false
        if (res.code !== 200) {
          this.$notification.warning({
            message: '提示',
            description: res.msg
          })
          return
        }
        const data = res.data
        console.log(data)
      }).catch(err => {
        this.codeLoad = false
        this.$notification.warning({
          message: '提示',
          description: err.message
        })
      })
    },
  }
}

# 9、fetch 方法

fetch 方法用于在渲染页面前填充应用的状态树(store)数据, 与 asyncData 方法类似,不同的是它不会设置组件的数据。

fetch 方法的第一个参数是页面组件的上下文对象 context,我们可以用 fetch 方法来获取数据填充应用的状态树。为了让获取过程可以异步,你需要返回一个 Promise,Nuxt.js 会等这个 promise 完成后再渲染组件。fetch无法在内部使用this获取组件实例,fetch是在组件初始化之前被调用。

<template>
  <h1>Stars: {{ $store.state.stars }}</h1>
</template>

<script>
  export default {
    fetch({ store, params }) {
      return axios.get('http://my-api/stars').then(res => {
        store.commit('setStars', res.data)
      })
    }
  }
</script>

# 10、跨域设置

安装:

npm install @nuxtjs/proxy

在nuxt.config中配置,如果不是使用自带的@nuxtjs/axios也可以直接配置代理。

export default {
  // 打包模式 static 静态预渲染 server 服务端渲染
  target: 'static',

  // nuxt内置模块
  modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios', // nuxt自带的axios请求模块
    '@nuxtjs/proxy'
  ],
  // Axios module configuration: https://go.nuxtjs.dev/config-axios
  axios: {
    // Workaround to avoid enforcing hard-coded localhost:3000: https://github.com/nuxt-community/axios-module/issues/308
    baseURL: '/',
    proxy: true
  },
  // 跨域设置
  proxy: {
    '/api': {
      // target: 'https://www.ahss.com.cn', // 目标接口域名
      target: 'http://192.168.100.14:3351', // 目标接口域名
      // pathRewrite: {
      //   '^/api': '/', // 把 /api 替换成 /
      // }
    }
  }
}

# 11、中间件

中间件允许您定义一个函数运行在一个页面或一组页面渲染之前,这样就可以像路径导航守卫一样在页面加载之前进行判断,如权限判断,动态验证是否能访问该页面。

中间件放置在 middleware 目录下。文件名的名称将成为中间件名称 (middleware/auth.js将成为 auth 中间件),创建好后可以在 nuxt.config.js 、 layouts 或者 pages 中使用中间件,在哪里引入就会在它之前执行。

添加权限验证中间件:

// middleware/auth.js
/**
 * @desc 权限验证
 * 判断哪些页面需要登录,跳转到登录接口
 * */ 
import storage from 'store'
export default function ({ app, route, redirect, store }) {
  const whiteList = ['/', '/product', '/news', '/about']
  const empowerList = ['/empower']
  if (process.client) {
    const token = storage.get('ACCESS_TOKEN')
    if (token) {
      if (!store.state.userInfo) {
        store.dispatch('getUserInfo')
      }
    } else {
      // eslint-disable-next-line no-lonely-if
      if (!whiteList.includes(route.path) && !empowerList.includes(route.path)) {
        redirect('/empower')
      }
    }
  }
}

放在nuxt.config中则所有路由都会执行此方法。

export default {
  target: 'static',
  // 中间件
  router: {
    middleware: 'auth'
  }
}

添加到指定的布局或者页面 pages/index.vue 者 layouts/default.vue

export default {
  middleware: 'auth'
}

# 12、Vuex使用

Nuxt中使用vuex不在需要创建vuex实例了,只需要在store目录下创建对应的js文件就可以自动识别,多个js文件会自动识别为多个模块,不需要引入。使用和正常在vue中使用一样,直接调用或者辅助函数都可以。

/**
 * @desc vuex状态管理器
 * vuex根模块
 * */ 

export const state = () => ({
  userInfo: {},
  isLogin: false,
  counter: 0
})

export const mutations = {
  setUserInfo(state, userInfo) {
    state.userInfo = userInfo
  },
  setIsLogin(state, isLogin) {
    state.isLogin = isLogin
  },
  increment(state) {
    state.counter++
  }
}

export const actions = {
  getUserInfo ({ commit }) {
    return new Promise((resolve, reject) => {
      this.$axios.get('/api/owner/info')
        .then((response) => {
          console.log(response)
          commit('setUserInfo', response.data.data)
          commit('setIsLogin', true)
          resolve()
        })
        .catch((err) => {
          reject(err)
        })
    })
  }
}

# 13、设置head

全局设置nuxt.config.js

export default {
  // 打包模式 static 静态预渲染 server 服务端渲染
  target: 'static',

  // Global page headers: https://go.nuxtjs.dev/config-head
  head: {
    title: 'Nuxt预渲染',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'keywords', name: 'keywords', content: 'Nuxt' },
      { hid: 'description', name: 'description', content: '这是一个Nuxt应用程序' }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
  }
}

设置当前页面head

export default {
  name: 'Mine',
  head () {
    return {
      title: 'Nuxt预渲染-我的',
      meta: [
        { hid: 'nuxtkeywords', name: 'keywords', content: '我的' },
        { hid: 'nuxtdescription', name: 'description', content: '我的' }
      ]
    }
  }
}