NuxtJS服务端渲染

背景

目前该前端项目是VUE编写的单页应用,如果开始推广,目前的架构对SEO的支持很不友好,为更好的支持推广,预研采用服务端渲染(SSR)十分的必要,并且静态化后页面的渲染速度也会有所提高。

经综合比对,选择采用开源成熟的NuxtJs框架进行服务端渲染。

NuxtJs简介

Nuxt.js 是一个基于Vue.js 开发SSR应用的一站式解决方案。同时,Nuxt.js 的热加载机制可以使开发者非常便捷的进行网站的开发。

优点:

  1. 基于 Vue.js

  2. 自动代码分层

  3. 服务端渲染

  4. 强大的路由功能,支持异步数据

  5. 静态文件服务

  6. ES6/ES7 语法支持

  7. 打包和压缩 JS 和 CSS

  8. HTML头部标签管理

  9. 本地开发支持热加载

  10. 集成ESLint

  11. 支持各种样式预处理器: SASS、LESS、 Stylus等

文件迁移说明

nuxtJs有一套自己的文件体系及约定,需要我们把原项目的文件迁移到新框架固定目录下。nuxtJs取消了src和public这些一级目录,相应的要把原项目src和public这些一级目录下面的二级文件目录变更成一级目录

  1. 原项目public/components文件及内容,转移到新框架下/components(nuxtJs约定目录,用于存放组件);
  2. 原项目public/pages文件及内容,转移到新框架下/pages(nuxtJs约定目录,页面目录 pages 用于组织应用的路由及视图。Nuxt.js 框架读取该目录下所有的 .vue 文件并自动生成对应的路由配置);
  3. 原项目public/router可以移除,nuxtJs会根据pages内部文件自动生成路由;
  4. 原项目public/store文件及内容,转移到新框架下/store(nuxtJs约定目录,用于组织应用的Vuex状态树文件);
  5. 原项目public/static文件及内容,转移到新框架下/static(nuxtJs约定目录,用于存放应用的静态文件,不会被webpack编译);
  6. 原项目涉及需要编译的静态文件及内容,如全局global.less等等,转移到新框架下/assets(nuxtJs约定目录,用于组织未编译的静态资源如 LESSSASS 或 JavaScript);
  7. 原项目public/api文件及内容,转移到新框架下/api;
  8. 原项目下plugins文件及内容可移除,里面只有elementUI的注册,在新框架初始化时,已安装及注册了elementUI。
  9. 原项目public/theme、public/utils、public/config文件及内容,转移到新框架根目录下即可。

文件修改说明

1. 使用框架自带@nuxt/axios及api方法改造

使用nuxtJs自带axios

nuxtJs自带axios,所以无需安装axios,只需要在全局配置里面引入后,即可使用。

引入自带的axios

modules: [

    // Introduction - Axios Module

    '@nuxtjs/axios',

],

api文件内方法的修改,主要为适配NuxtJS自带axios模块

之前我们都是在api/...js中对某个方法进行维护,并导出。然后在页面中引入和使用。

例子

// api/user.js

export async function getUser() {

  return request.get(`/api/get/user`)

}

// 页面.vue

import { getUser } from '@/api/user'

create:{

    getUser().then(res=>...)

}

在新的书写方式中,采用插件化的写法

​
// api/user.js

export default ({ $axios }, inject) => {

  inject('apiUser', {

    getUser() {

      return $axios.get(`/api/get/user`).catch(() => Promise.resolve({}))

    }

    ...

  })

}

// nuxt.config.js 注册

export default {

    plugins: [

        '~api/user', // 插件注册

    ],

}

// 页面.vue

async asyncData({app}){

    app.$apiPlugins.getUser().then(res=>{...})

},

create:{

    this.$apiUser.getUser().then(res=>{...})

}

​

通过上述修改,使用NuxtJS自带的axios模块进行网络请求,对相应的api文件和页面使用方式进行改造,为接下来第二步的拦截器修改提供了基础。

2. 拦截器修改(request.js改造)

原项目的全局配置及拦截操作放在public/utils/request.js中,正常情况下,在新框架也可以这样使用。

但是经排查发现request.js涉及到了操作store,目前的写法及用法在NuxtJs中是无法找到和使用store的,因此要对文件进行插件化改造(NuxtJs自有的插件化体系)

拦截器插件化改造

插件可以注入两个参数context和inject

context:全局上下文,包含当前执行环境下的变量和方法(app,store,route,params,query,env,isDev,isHMR,redirect,error,$config,$axios

inject:方法,它是 plugin 导出函数的第二个参数。将内容注入 Vue 实例的方式与在 Vue 应用程序中进行注入类似。系统会自动将$添加到方法名的前面

context中暴露的$axios是第一步中全局注册的@nuxtjs/axios模块。

// plugins/interceptor.js   使用这种方式才能拿到store,传统的封装request,无法拿到store
 
// axios全局拦截
// 变量名从app解构
export default function ({ store, route, redirect, $axios }) {
  // options
  // $axios.defaults.baseURL = '/'
  if (process.server) {
    // 获取服务端的token
    console.log('server')
  }
  if (process.client) {
    // 获取客户端token
    console.log('client')
  }
  // 通过context中的store可以获取到当前状态树
  const token = store.state.user.erpUserInfo && store.state.user.erpUserInfo.jti
  // 请求监听
  $axios.onRequest(config => {
    console.log('Making request to ' + config.url)
  })
  // 响应监听
  $axios.onResponse(response => {
    console.log('response')
  })
  // 异常监听
  $axios.onError(error => {
    console.log(error)
    const code = parseInt(error.response && error.response.status)
    if (code === 400) {
      redirect('/400')
    }
  })
}

文件创建完成后需要添加到全局插件中

// 在全局引入当前插件
// nuxt.config.js
export default {
    plugins: [
        '~plugins/interceptor', // 插件注册
    ],
}

通过上述配置,实现了添加拦截器对请求或响应数据进行处理,同时支持在拦截器中可以操作状态树

3. 路由守卫及鉴权

目前的项目还没有涉及到鉴权,此内容为提前规划部分

NuxtJS提供一个中间件目录middleware,可以在里面注册中间件并在全局或页面中使用。

// 新建 middleware/auth.js
export default function (context) {
  const { app, store } = context
  // 在下面的内容中做鉴权、拦截跳转等处理
  if (!store.state.authUser) {
    return redirect('/login')
  }
}

中间件创建后可以根据情况在全局使用或页面使用

// nuxt.config.js
export default{
    router: {
        middleware: ['auth'],
    },
}
// 页面使用

// pages/user.vue
<script>
export default {
    // 引用权限中间件
    middleware: 'auth',
    ...
}
</script>

通过上述操作,可以在局部和全局使用中间件对权限进行鉴权

4. 项目配置

安装

npx create-nuxt-app <项目名>

Koa

Element UI

Jest

Universal

axios

EsLint

Prettier

5. 使用Vuex持久化插件(vuex-persistedstate)

vuex可以进行全局的状态管理,但刷新后数据会消失。传统的方案是手动在localStorage或sessionStorage或其它存储方式中获取和设置值,并同步到vuex的state中,缺点是手动写比较麻烦。

vuex-persistedstate原理其实也是结合了存储方式,只是统一的配置就不需要手动每次都写存储方法

安装: npm i --save vuex-persistedstate

在nuxt.config.js 配置文件中注册插件,注意修改ssr为false。否则会在服务端运行,且运行时找不到window变量并报错。

// nuxt.config.js 配置文件
plugins: [
    { src: '~/plugins/vuex-persistedstate', ssr: false },
],
 
// 新建文件 plugins/vue-persistedstate.js
import createPersistedState from "vuex-persistedstate";
export default (context) => {
  createPersistedState({
    reducer(obj) {
      // 其中 username authority 为需要自动存储的 state
      const { username, authority } = obj;
      return { username, authority };
    },
  })(context.store);
};
按照以上方法进行配置后,就不用再对store的持久化存储进行额外的维护。日常开发只用关注store的读写,减少了日常开发的工作量。

6. 使用缓存优化页面二次访问速度,减少服务器压力

虽然 Vue 的 SSR 非常快,但由于创建组件实例和 Virtual DOM 节点的成本,它无法与纯粹基于字符串的模板的性能相匹配。在 SSR 性能至关重要的情况下,合理地利用缓存策略可以大大缩短响应时间并减少服务器负载。

使用缓存的目的:1. 减轻api服务器请求压力;2.减轻Nuxt的服务器渲染压力;

对于某些页面常用的,且不经常变化的api请求,可以采用lru-cache进行缓存

通过对一定时间内页面的首次api的请求进行缓存处理,设置缓存的有效期(最大缓存时长和最小缓存时长)。再次访问页面时,如果缓存没过期的话,可以直接拿存储的数据,较少api请求和服务器压力。由于中间少了请求api及等待返回的流程,也能减少整个页面的渲染时间,优化访问速度。

实施方案:

import LRU from 'lru-cache'
const CACHED = new LRU({
  max: 100, // 缓存队列长度
  maxAge: 1000 * 60 // 缓存时间
})
export default {
    async asyncData(){
        // 判断有缓存直接返回缓存
        if (CACHED.has('indexData')) {
          let indexData = CACHED.get('indexData')
          indexData = JSON.parse(indexData)
          return indexData
        }
        .....
        const res = {...}
        // 存储缓存            
        CACHED.set('indexData', JSON.stringify(res))
        return res            
    }
}

通过对应用市场的测试,未添加api缓存之前,页面每次访问及刷新时:dom渲染完成时间(1.97s),页面加载完成时间/Load(2.41s)

 添加api缓存方案后,首次访问页面时间几乎不变

 添加api缓存方案后,二次访问页面:刷新后页面的加载时间为:dom渲染完成时间(1.49s),页面加载完成时间/Load(1.83s)

本地测试结果:采用api缓存方案后,页面二次访问dom加载时间缩短0.59s,页面加载时间缩短0.67s,提升了大约26%左右的页面响应速度。

ps:对于部分网页进行服务端的缓存,可以获得更好的渲染性能,但是缓存又涉及到一个数据的及时性的问题,所以在及时性和性能之间要有平衡和取舍。

asyncData函数优化,避免里面接口报错影响页面渲染

1. 方案一:直接在asyncData里面对请求进行catch,并返回空对象

// $axios.get('/api/data1').catch(() => Promise.resolve({}))
async asyncData({ $axios }) {
 // 1、增加catch处理,是为了让服务端,客户端运行时不报错,特别是防止服务端运行时不报错,不然页面就挂了
 // 2、catch函数返回一个resolve空字面量对象的Promise,表明dataPromise1的状态未来始终是resolved状态
 const dataPromise1 = $axios.get('/api/data1').catch(() => Promise.resolve({}))
  
 const dataPromise2 = $axios.get('/api/data2').catch(() => Promise.resolve({}))
 const dataPromise3 = $axios.get('/api/data3').catch(() => Promise.resolve({}))
 const dataPromise4 = $axios.get('/api/data4').catch(() => Promise.resolve({}))
 const dataPromise5 = $axios.get('/api/data5').catch(() => Promise.resolve({}))
 const dataPromise6 = $axios.get('/api/data6').catch(() => Promise.resolve({}))
 const dataPromise7 = $axios.get('/api/data7').catch(() => Promise.resolve({}))
 const dataPromise8 = $axios.get('/api/data8').catch(() => Promise.resolve({}))
  
 // 保证apiData有数据
 const apiData = await new Promise(resolve => {
    Promise.all([
    dataPromise1, dataPromise2, dataPromise3, dataPromise4,
    dataPromise5, dataPromise6, dataPromise7, dataPromise8,
   ])
   .then(dataGather => {
        resolve({
        data1: dataGather[0],
        data2: dataGather[1],
        data3: dataGather[2],
        data4: dataGather[3],
        data5: dataGather[4],
        data6: dataGather[5],
        data7: dataGather[6],
        data8: dataGather[7],
       })
   })
   })
    return apiData
 }
}

2. 方案二:根据目前代码实际情况,把catch放在api请求里

// api/plugins.js
//原代码
getProductCategory(params) {
  return $axios.get(`${api}/v1/product-categories/all/all`, { params })
},
// 改造后
getProductCategory(params) {
  return $axios.get(`${api}/v1/product-categories/all/all`, { params }).catch(() => Promise.resolve({}))
},

这样的话页面就不需要做额外操作了,保证页面代码的简洁

通过上述操作,避免了asyncData生命周期内请求api报错时,导致前端页面无法访问的问题。

添加错误日志打印 nuxt-winston-log

npm i nuxt-winston-log

nuxt-winson-log的默认保存路径是当前文件夹下的logs文件夹。修改配置让日志保存在其他路径:

// nuxt.config.js
export default {
 modules:[
     'xxxx其他modules',
     [
       'nuxt-winston-log',
       {
          logPath:
             process.env.npm_lifecycle_event === 'build' ||
             process.env.NODE_ENV === 'development'
               ? './logs'
               : `/data/weblog/nodejs/${process.env.npm_package_name}`,
           logName: `${process.env.npm_package_name}.log`
       }
     ]
 ]
}

区分了开发和生产的日志存放目录。 同时使用npm_lifecycle_eventNODE_ENV而不是process.env.NODE_ENV === 'production'去做判断是因为构建过程中的process.env.NODE_ENV也是production,会因为构建机器上没有这个日志存放目录导致构建失败。

对日志做分级

// nuxt.config.js
import path from 'path';
import { format, transports } from 'winston';
const { combine, timestamp } = format;
 
// 日志存放路径
const infoLogPath = path.resolve(process.cwd(), './logs', `info.log`);
const errorLogPath = path.resolve(process.cwd(), './logs', `error.log`);
 
export default {
  modules: ['nuxt-winston-log'],
  winstonLog: {
    loggerOptions: {
      transports: [
        new transports.File({
          // format: combine(timestamp()),
          format: format.combine(
            format.timestamp({
              format: 'YYYY-MM-DD HH:mm:ss'
           }),
            format.errors({ stack: true }),
            format.splat(),
            format.json()
         ),
          level: 'info',
          filename: infoLogPath,
          maxsize: 5 * 1024 * 1024  // 这个是限制日志文件的大小
       }),
        new transports.File({
          // format: combine(timestamp()),
          format: format.combine(
            format.timestamp({
              format: 'YYYY-MM-DD HH:mm:ss'
           }),
            format.errors({ stack: true }),
            format.splat(),
            format.json()
         ),
          level: 'error',
          filename: errorLogPath,
          maxsize: 5 * 1024 * 1024
       })
     ]
   }
 },
}
// plugins/interceptor.js
export default function ({ store, route, redirect, $axios, $winstonLog }) {
    $axios.onResponse(response => {
        // 打印日志 先判断是否有$winstonLog,因为该变量只在服务端存在
        if ($winstonLog) {
          $winstonLog.info(`[${response.status}] ${response.request.path}`)
       }
   }   
    $axios.onError(error => {
        // 打印日志 先判断是否有$winstonLog,因为该变量只在服务端存在              
        if ($winstonLog) {
          $winstonLog.error(`[${error.status}] | ${error.request.path} | ${error.message}`)
          $winstonLog.error(error.response && error.response.data)
       }
 }
}

这样配置就可以实现infoerror日志分级了!如果发现启动的时候两个日志文件没有生成,可以检查一下设置的保存路径是否存在。

通过日志打印可以方便的了解到页面的访问及错误情况,通过日志分析,可以更方便的开发中进行针对性的优化和错误定位。

ps: 日志目前仅在本地调试使用,如果开发环境已有相应的配置,则此项可以忽略,避免重复的日志输出。

前端错误页面配置

1. 定制化错误页面

通过编辑 layouts/error.vue 文件来定制化错误页面,这个布局文件不需要包含 <nuxt/> 标签。你可以把这个布局文件当成是显示应用错误(404,500 等)的组件。

<template>
  <div class="container">
    <h1 v-if="error.statusCode === 404">页面不存在</h1>
    <h1 v-else>应用发生错误异常</h1>
    <nuxt-link to="/">首 页</nuxt-link>
  </div>
</template>
 
<script>
  export default {
    props: ['error'],
    layout: 'blog' // 你可以为错误页面指定自定义的布局
 }
</script>

2. 拦截不匹配的路由跳转至404

 

// nuxt.config.js
router: {
    extendRoutes(routes, resolve) {
      routes.push({
        name: 'custom',
        path: '*',
        redirect: '/404'
     })
   }
 },

以上定制化是用于显示应用错误(404,500 等)的组件,客户端遇到错误问题,会默认显示此页面。

添加百度统计

添加百度统计后可以随时监控网站的很多信息,比如流量来源,停留网页时间等很多重要信息。通过这些统计数据,可以了解自己网站的运营情况。

// plugins/hm.js
   export default ({ app: { router }, store }) => {
     /* 每次路由变更时进行pv统计 */
     router.afterEach((to, from) => {
       /* 告诉增加一个PV */
       try {
         window._hmt = window._hmt || [];
         window._hmt.push(["_trackPageview", to.fullPath]);
       } catch (e) {}
     });
   };
   // nuxt.config.js
   head: {
       script: [
       {src: 'https://hm.baidu.com/hm.js?****'},/*引入百度统计的js*/
       ]
   },
   plugins: [
      '~plugins/hm.js',/*百度统计*/
   ],

ps: 生产环境使用需要先去百度统计官网注册申请。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值