乐趣区

关于vue.js:Nuxtjs实战经验总结

近期公司成立了一个新我的项目,须要做 SEO。做过前后端半拆散(局部数据是后端模板渲染,局部用 AJAX 获取)我的项目的同学都晓得跟后端人员把代码写在同一个页面有多好受,我的项目不易保护且开发体验很差,因而采纳了 Nuxt.js

SEO

大家都晓得,网页在搜索引擎中能被搜寻进去是因为他们有本人的爬虫,能够爬取网页中的内容并保留下来,然而它们只会保留网页中的第一个申请返回的内容,即 HTML 文档。

在传统的前后端不拆散网站中,在浏览器上查看源代码,能够看到网页上有什么内容,源代码中就有与之对应的代码。随着前后端拆散开发越来越风行,也衍生了越来越多的单页面利用(SPA),在浏览器中关上一个 SPA 的源代码,往往只能看到寥寥无几的几个标签,其余内容全都是由 JS 渲染的,也就是客户端渲染。因而单页面利用的一大痛点就是搜索引擎无奈收录,有痛点就会有相应的计划产生,呈现了 SSR。

SSR

SSR 意思是服务端渲染,乍一看会认为是回到以前的前后不拆散开发模式,实际上相比以前,客户端与服务端之间多了一个中间层,用于接收数据并渲染页面返回到客户端。



有了中间层帮客户端渲染页面,在搜索引擎爬到页面时,就解决了前后端拆散页面不收录的的痛点。不仅如此,因为中间层也是一个服务端,前端工程师能够做更多事儿了,比方简略解决数据、做代理等等。

Nuxt.js

Vue 官网提供了 vue-server-renderer 模块实现 SSR,而 Nuxt.js 是对其封装后的框架,用于提供一个开箱即用的 NodeJS SSR 服务。

生命周期

通过在地址栏输出链接,或者从其余网站点击链接跳转到 Nuxt 站点的页面时(首屏拜访),出现的第一个页面的数据会由中间层向服务端发动申请,而后渲染 Vue 组件,再将渲染实现后生成的 HTML 文档返回到客户端。随后点击页面的 <NuxtLink> 组件或者通过 vue-router 的 push,replace 等办法跳转页面都是在客户端实现的,除非刷新浏览器,否则不会再次拜访中间层服务器。因而在页面中申请服务端的数据有可能是中间层发动的,也有可能是客户端发动的。

拜访 Nuxt 站点的页面时,会经验以下几个生命周期,咱们能够在不同的节点对站点的行为做解决

  1. 依照 nuxt.config.js 中的 plugins 配置程序执行插件。
  2. nuxtServerInit: 如果 Nuxt 利用中应用了 Vuex,会运行 actions 中的 nuxtServerInit 函数,用于在进入页面之前将数据填充到 Vuex 中。
  3. Middleware: 按程序执行 nuxt.config.js 中配置的 router 中间件、layout 中间件、page 中间件,能够用来对用户做权限验证。
  4. validate(): 返回 truefalse 或者 Promise,返回false 时,将会跳转到谬误页面,次要用于验证动静路由的参数。
  5. asyncData(): 返回一个对象,执行结束之后 Nuxt 会将返回的对象合并到 Vue 实例的 data 属性中,用于在拜访到页面之前调用接口获取数据,这个钩子取代了 SPA 中罕用的 created 钩子。
  6. beforeCreate: Vue 组件生命周期钩子。
  7. created: Vue 组件生命周期钩子。
  8. fetch(): 与 asyncData 一样,也是用于调用接口获取数据,不同的是它不会将返回值合并到 data,个别用于对本地数据处理,比方 Vuex,值得一提的是,这个钩子在 Vue 实例创立之后执行,因而能够用 this 拜访 Vue 实例。
  9. 执行 beforeMountmounted 等其余 Vue 的生命周期钩子。

以上生命周期中,只有前两个是齐全在中间层执行的,其余的有可能在中间层执行,也有可能在客户端执行,这次要取决于页面是首屏渲染还是站内跳转,因而搞懂哪些代码什么时候在什么环境运行十分重要。

插件

vant-ui

我的项目中应用了 vant ui 作为组件库,Nuxt 我的项目中没有相似 main.js 的入口文件,如果须要装置依赖,须要在 plugins 中申明。

每次拜访 Nuxt 站点的首屏时,都会将 nuxt.config.js 中申明的 plugins 执行一遍,从而让开发这能够把本人所需的插件注册到利用中

// nuxt.config.js
module.exports = {
    ...,
    plugins: ['@/plugins/vant-ui'],
    ...
}

在根目录的 plugins 目录下须要创立一个对应的文件

// /plugins/vant-ui.js
import Vue from 'vue'
import {
  Button,
  Search,
  Toast
} from 'vant'

Vue.use(Button)
Vue.use(Search)
Vue.use(Toast)

为了按需引入,还须要装置 babel-plugin-import 和进行相干配置

// nuxt.config.js
module.exports = {
  build: {
    analyze: true,
    transpile: [/vant.*?less/],
    babel: {
      // 按需引入配置
      plugins: [
        [
          'import',
          {
            libraryName: 'vant',
            style: name => `${name}/style/less.js`
          },
          'vant'
        ]
      ]
    },
    loaders: {
      less: {
        lessOptions: {
          modifyVars: {
            // 此处能够批改 vant 款式中的 less 变量,从而自定义 ui 款式
            '@button-primary-background-color': '#000'
          }
        }
      }
    }
  }
}

axios

Nuxt 官网提供了专门封装的 @nuxtjs/axios 模块,看过文档后发现与平时的用法略有不同,闲麻烦就还是用了原汁原味的 axios,毕竟此前也做了进一步的封装 (官网强烈推荐的模块尽量能用就用,像我这种不听话的就会默默踩坑,通过摸索才悟出了上面这段话)。然而与 SPA 不同的是,axios 发动的申请可能是在中间层服务端,也可能是在客户端,这取决于是否是首屏渲染(在下面生命周期有提到),因而须要在代码中利用process.clientprocess.server对相应的环境做出判断和解决。

import axios from 'axios'
import {Toast} from 'vant'

const {CancelToken} = axios

// 用于获取在 nuxt.config.js 配置中的 baseURL
const nuxtConfig = require('../nuxt.config')

// 让申请函数带有 axios cancelToken
export const cancelToken = fn => {const newFn = (...arg) => {
    // 每个 cancelToken 只能应用一次,之后会放弃状态
    // 因而每次发动申请都创立一个新的 cancelToken
    const source = CancelToken.source()
    newFn.token = source.token
    newFn.cancel = source.cancel

    return fn(...arg)
  }

  return newFn
}

// 创立实例
function createAxios () {
  const instance = axios.create({
    baseURL: '/api', // 中间层代理地址
    timeout: 10000
  })

  // 申请拦挡
  instance.interceptors.request.use(config => {
    // 服务端不须要代理
    if (process.server) {config.baseURL = nuxtConfig.env.baseUrl}
    return config
  }, err => {return Promise.reject(err)
  })

  // 响应拦挡
  instance.interceptors.response.use(res => {const { data} = res

    if (data.code !== 200) {if (process.server) {
        // 在 plugins 中,我把 context 挂载到了 axios 实例上
        // 在服务端发动申请出错时,能够跳转到谬误页面
        instance.nuxtContext.error({
          statusCode: 500,
          message: ''
        })
      } else {Toast(data.msg)
      }
      return Promise.reject(res)
    }

    return data.data
  }, err => {const { response} = err

    // 服务端和客户端有不一样的错误处理形式
    if (process.client) {if (response) {switch (response.status) {
          case 401:
            // 未登录
            Toast('请先登录')
            break
          case 403:
            // 操作被回绝(没有相应权限)
            Toast('您的操作被回绝')
            break
          case 404:
            Toast('未找到资源')
            break
          case 500:
            Toast('零碎出错了')
            break
        }
        return Promise.reject(response)
      }

      // 勾销申请
      if (axios.isCancel(err)) {console.log('申请勾销')
        return Promise.reject(err)
      }

      // 申请超时
      if (err.message?.includes('timeout')) {Toast('网络超时,请重试')
        return Promise.reject(err)
      }

      if (!window.navigator.onLine) {
        // 断网
        Toast('请查看网络')
        return Promise.reject(err)
      } else {Toast('未知谬误')
        return Promise.reject(err)
      }
    } else {console.log(response)

      instance.nuxtContext.error({
        statusCode: response.status,
        message: ''
      })
    }
  })

  return instance
}

export default createAxios()

增加一个插件将 nuxt context 挂载到 axios 实例上,用于出错时跳转到谬误页面,此外须要记得在 nuxt.config.js 中申明此插件。

// /plugins/axios.js
import axios from '@/utils/request'

// 将 context 对象挂载到 axios 实例上
export default context => {axios.nuxtContext = context}

Vuex 长久化

在 SPA 中,通常会将 token 存储在 localStorage 中,然而拜访 SSR 站点首屏时,须要在中间层做登录等权限验证,无奈获取到客户端存在 localStorage 中的数据。然而能够将其存在 cookie 中,利用 cookie 的个性,在拜访站点时将其带到中间层,于是中间层就能够获取 cookie 中的数据做权限验证,并且将其填充进 Vuex 供客户端应用。

首先装置 vuex-persistedstatejs-cookie,把 Vuex 中的数据同步到 cookie,而后写一个插件将 vuex-persistedstate 注入到 Vuex 中(别忘了在 nuxt.config.js 中申明)。

// /plugins/vuex-persistedstate.js
import createPersistedState from 'vuex-persistedstate'
import Cookies from 'js-cookie'

export default ({store}) => {
  //“将 Vuex 中的数据同步到 cookie 中”这种事只有客户端会做
  if (!process.client) {return}

  createPersistedState({
    // 通过配置批改 vuex-persistedstate 的读写行为
    // 将操作指标改为 cookie
    storage: {
      getItem: key => {return Cookies.get(key)
      },
      setItem: (key, value) => {
        Cookies.set(key, value, {expires: 365})
      },
      removeItem: key => {Cookies.remove(key)
      }
    }
  })(store)
}

而后在 nuxtServerInit() 这个生命周期钩子中读取拜访时的 cookie。

// /store/index.js
import {SET_USER_INFO} from './mutation-types'

export const state = () => ({token: ''})

export const mutations = {[SET_TOKEN] (state, token) {state.token = token}
}

export const actions = {nuxtServerInit ({ commit}, {req}) {
    // 通过 Nuxt context 获取 cookie 有很多办法,这里我用了 cookie-parser
    const {vuex} = req.cookies

    if (!vuex) {return}

    commit(SET_TOKEN, JSON.parse(vuex).token)
  }
}

这样一来,就能够在后续的 Middlewarevalidate() 等钩子通过context.store 获取到所需的数据了。

此外,为了方便管理,我在文件中引入了 /store/mutation-types.js,默认状况下 Nuxt 会将/store 目录下的 .js 文件当作一个 Vuex 的模块,因而须要配置一下 Nuxt 的疏忽文件,让它疏忽掉mutation-types.js

# /.nuxtignore
# ignore store mutation types
store/mutation-types.js

应用 SCSS

为了更难受地编写 CSS,当然须要一个 CSS 预处理器!为了能让通用的 .scss(变量、mixin 等)可能用在任何组件中应用,须要借助@nuxtjs/style-resources 模块实现,第一步装置,第二步配置。

// nuxt.config.js
module.exports = {
  ...,
  buildModules: ['@nuxtjs/style-resources'],
  // 配置全局的 function,mixin,variable
  styleResources: {
    scss: [
      // 留神程序!被前面文件依赖的文件须要放在后面
      '@/assets/scss/_variables.scss',
      '@/assets/scss/_functions.scss',
      '@/assets/scss/_mixins.scss'
    ]
    // sass: [],
    // less: [],
    // stylus: []}
  ...
}

代理

因为浏览器的平安机制,会拦挡跨域的 AJAX 申请,为了解决跨域问题,能够设置代理,而这次的代理能够不是 Nginx 了,能够间接用中间层作为代理,为客户端申请数据。装置完 @nuxtjs/proxy 之后进行配置即可。

// nuxt.config.js
module.exports = {
  ...,
  modules: ['@nuxtjs/proxy'],
  proxy: {
    '/api': {
      target: 'https://xxx.com',
      pathRewrite: {'^/api': '/'}
    }
  }
  ...
}

自定义入口文件

入口文件的次要作用是启动服务和初始化 Nuxt 外围类,Nuxt 曾经将入口文件封装好并暗藏了,看不到也改不了,不过咱们能够本人把这两件事件做了,让入口文件在本人掌控之下,变得更具备扩展性。

// 根目录创立 server.js
const {loadNuxt, build} = require('nuxt')
const Express = require('express') // 应用 express 启动服务,这样就能够对中间层做更多的解决,比方应用更多中间件等等
const cookieParser = require('cookie-parser') // 后面 Vuex 长久化中应用的 cookie-parser 就是这儿来的

const app = Express()

// 自定义入口文件之后,nuxt.config.js 的 server 选项会生效,须要手动调用
const config = require('./nuxt.config')

const isDev = process.env.NODE_ENV !== 'production'
const {host, port} = config.server

async function start() {
  // We get Nuxt instance
  const nuxt = await loadNuxt(isDev ? 'dev' : 'start')

  app.use(cookieParser())
  // Render every route with Nuxt.js
  app.use(nuxt.render)

  // Build only in dev mode with hot-reloading
  if (isDev) {build(nuxt)
  }

  // Listen the server
  app.listen(port, host)
  console.log('Server listening on `localhost:' + port + '`.')
}

start()

而后在 package.json 中批改一下启动命令。

{
  "scripts": {
    ...,
    "start": "node server.js",
    ...
  }
}

结语

因为我的项目还处在初期阶段,能不能持续做上来还不晓得,所以我的项目规模十分无限,还有许多没有波及到的点,如果您有补充或者发现有误的中央,请帮我纠正,谢谢观看。

退出移动版