从0到1之基于nuxtjs的SSR实践

631 阅读7分钟

SEO

SEO 代表搜寻引擎最佳化/搜寻引擎优化(英文全名Search Engine Optimization,简称SEO),是指通过了解搜寻引擎的自然排名的算法逻辑,以提高目标网站在有关搜寻引擎内排名的方式。网站的 SEO 至关重要,它可以让你的网站获得更好的排名和流量,从而提高网站知名度。现代前端开发模式下seo优化方案无非从以下几个方面展开:

  1. 多页面模式
  2. title、描述、关键词等信息
  3. 网站内容是怎么来的?

解决方式一:预渲染

预渲染图解.png

预渲染适合做可能某几个页面需要做seo类型的企业项目,我们通过插件 prerender-spa-plugin实现预渲染,步骤如下:

  1. vue项目中安装prerender-spa-plugin: npm install prerender-spa-plugin -S
  2. vue.config.js进行配置, 如下所示,对三个页面 / /about /contact做预渲染的配置,在打包编译之后会生成三个独立的 html 静态页面:
const path = require('path');
const PrerenderSPAPlugin = require('prerender-spa-plugin');

module.exports = {
  publicPath: './',
  configureWebpack: {
    plugins: [
      new PrerenderSPAPlugin({
        staticDir: path.join(__dirname, 'dist'),
        routes: [
          '/',
          '/about',
          '/contact',
        ],
      }),
    ],
  },
};
  1. 修改预渲染页面的title、描述关键词:下载插件 vue-meta-info, npm install vue-meta-info -S, 到页面组件中配置, 以页面 contact.vue为例:
// main.js 中引入
import MetaInfo from 'vue-meta-info'
Vue.use(MetaInfo)

// contact.vue
<template>
  <h2>Contact Page</h2>
</template>

<script>
export default {
  metaInfo: {
    title: '联系我们',
    meta: [{
      name: 'keyWords',
      content: 'My Example App'
    }]
  }
}
</script> 
  • 优点:
  1. 打包多页面
  2. 可以解决每个页面单独生成title描述关键词 vue-meta-info
  3. 接口数据是在html生成之前就放在页面上的,爬虫可以抓取到内容
  • 存在的问题 (无法解决获取动态数据渲染的问题):
  1. 预渲染无法配置动态路由
  2. 如果title描述关键词来自于接口的数据,配置到meta-info也是不行的

解决方式二:服务端渲染(通过SSR)

服务端渲染.jpg 基于nuxtjs的服务端渲染,适合做一个项目中可能所有页面都需要做seo。如上图所示,我可以把这个框架理解成基于 nodejs 服务上的一个前端开发框架,最终是通过 nodejs 来部署并做服务端渲染。虽说是服务端渲染项目,实际上关注的重点还是应用层的ui渲染。

nuxtjs的使用实践:

一、nuxtjs安装过程中的选项

  • Programming language : 程序设计语言
  • Package manager : 包管理器
  • UI framework : ui框架
  • Nuxt.js modules : nuxt的js模块
  • Linting tools : 代码校验工具
  • Testing framework : 测试框架
  • Rendering modules : 渲染模式
  • Deployment target : 部署目标
  • Development tools : 开发工具
  • Version control system : 版本控制工具

二、目录结构

  • pages : 存放页面的文件目录,类似于vuejs框架中的 src/views
  • components : 存放组件的文件目录,类似于vuejs框架中的 src/components
  • static : 存放静态资源的文件目录,类似于vuejs框架中的 src/assets
  • store : vuex状态树,类似于vuejs框架中的 src/store
  • nuxt.config.js : 全局的配置文件,类似于vuejs框架中的 vue.config.js

三、服务端生命周期

Nuxt 完整生命周期分为服务端渲染和客户端渲染:

【服务端渲染】
  1. 全局
  • nuxtServerInit 第一个:nuxt中第一个运行的生命周期
  • middleware 第二个:中间件,类似于原框架的路由导航守卫
  1. 组件
  • validate 是用来校验url参数符不符合
  • asyncData Nuxt专属生命周期,可用于数据请求,只有page可用,子组件内部不可用
  • beforeCreate Vue生命周期,但是服务端会执行(不可用于数据请求,数据请求相关操作会在客户端执行)
  • created Vue生命周期,但是服务端会执行(同上)
  • fetch Nuxt专属生命周期,可用于数据请求,page和子组件都可用
【客户端渲染】

beforeCreate、created、beforeMount、mounted、... (其他Vue后续生命周期)

1. nuxtServerInit (store ,context){} (参数1:vuex上下文;参数2:nuxt上下文)

nuxtServerInit 在nuxt生命周期中第一个执行,通常用来做为登陆鉴权的操作。在如下代码中,在该生命周期方法中通过 commit 方法调用 setToken 设置token信息,页面中可以就可以获取对应的state值(注:不同生命周期中获取的方式不一样)

//  store/index.js
export const state = {
    token:''
}

export const mutations = {
    setToken(state,token){
        state.token = token;
    }
}

export const actions = {
    nuxtServerInit(store , context ){
       store.commit('setToken','nuxtServerInit');
    }
}
// 分别在获取asyncData 和 created 生命周期中获取state:
asyncData({app ,store, params}){
    console.log( 'asyncData', store.state )
},
created(){
    console.log( 'created', this.$store.state);
},

// 获取日志信息:
// asyncData { token: 'nuxtServerInit' }
// created { token: 'nuxtServerInit' }
2. middleware ({store, route, redirect, params, query, req, res}){} 类似于vue中的导航守卫

全局配置:需要在nuxt.config.js进行配置, 同时在 middleware 文件目录下创建和配置中的名称一样的js 文件,例如配置 middleware/auth.js 文件

// nuxt.config.js
router:{
    middleware:'auth'
}
// auth.js
export default function({store,route,redirect,params,query,req,res}){
    const token = store.state.token;
    console.log( 'middleware 中获取到 token', token );
}

// 获取日志信息:
// middleware 中获取到 token nuxtServerInit

局部配置:在页面组件中引入 middleware: 'xxxx(js文件名)',同时在 middleware 文件目录下创建和配置中的名称一样的js 文件,例如配置 middleware/auth.js 文件,此时就需要屏蔽全局配置中的 middleware

export default {
  data () {
      return {}
  },
  methods:{
      //
  },
  name: 'IndexPage',
  middleware: 'auth',
  ......
}
3. validate ({app, params,query}) {} 校验url参数 

validate 可以让你在动态路由对应的页面组件中配置一个校验方法用于校验动态路由参数的有效性。第一个参数为上下文 app。我们能够在该方法里面访问实例上的方法 this.methods.xxx。生命周期可以返回一个 Boolean,为true则进入路由,为false则停止渲染当前页面并显示错误页面:

// 调用校验函数方法
export default {
  validate({ params, query }) {
    return this.methods.validateParam(params.type)
  },
  methods: {
    validateParam(type){
      let typeWhiteList = ['index', 'list']
      return typeWhiteList.includes(type)
    }
  }
}
// 还可以返回一个 Promise
export default {
  validate({ params, query, store }) {
    return new Promise((resolve) => setTimeout(() => resolve()))
  }
}
// 还可以在验证函数执行期间抛出错误:
export default {
  async validate ({ params, store }) {
    // 使用自定义消息触发内部服务器500错误
    throw new Error('Under Construction!')
  }
}
4. asyncData({app, store,params}){} pages中的页面来请求数据的

asyncData 是最常用最重要的生命周期,同时也是服务端渲染的关键点。该生命周期只限于页面组件调用,第一个参数为上下文 app。它调用的时机在组件初始化之前,运作在服务端环境。所以在 asyncData 生命周期中,我们无法通过 this 引用当前的 Vue实例,也没有 window 对象和 document 对象,这些是我们需要注意的。

一般在 asyncData 会对主要页面数据进行预先请求,获取到的数据会交由服务端拼接成 html 返回前端渲染,以此提高首屏加载速度和进行 seo 优化。

export default {
  async asyncData({ app }) {
    let list = await app.$axios.getIndexList({
      pageNum: 1,
      pageSize: 20
    }).then(res => res.s === 1 ? res.d : [])
    // 返回数据
    return {
      list
    }
  },
  data() {
    return {
      list: []
    }
  }
}
5. fetch({app,store,params}){}

fetch 方法用于在渲染页面前填充应用的状态树(store)数据, 与 asyncData 方法类似,不同的是它不会设置组件的数据。为了让获取过程可以异步,你需要返回一个 PromiseNuxt.js 会等这个 promise 完成后再渲染组件

export default {
  fetch ({ store, params }) {
    return axios.get('http://my-api/stars')
    .then((res) => {
      store.commit('setStars', res.data)
    })
  }
}
6. 其他生命周期
  • 服务端和客户端共有的生命周期:beforeCreate、created
  • 客户端生命周期:beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed

四、路由

一、Nuxt.js会根据pages目录结构自动生成 vue-router 模块的路由配置。

要在页面之间使用路由,建议使用<nuxt-link> 标签。例如下面的的跳转方式使用:

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

在项目代码中,如果假设 pages 的目录结构如下,则有对应会自动生成的的路由配置;如果在 Nuxt.js里面定义带参数的动态路由,需要创建对应的以下划线作为前缀的 Vue文件或目录。

// 项目的目录结构
pages/
--| user/
-----| index.vue
-----| one.vue
--| search/
-----| _id.vue
--| _slug/
-----| comments.vue
-----| index.vue
--| index.vue

// nuxtjs 自动生成的路由配置信息
router: {
  routes: [
    {
      name: 'index',
      path: '/',
      component: 'pages/index.vue'
    },
    {
      name: 'user',
      path: '/user',
      component: 'pages/user/index.vue'
    },
    {
      name: 'user-one',
      path: '/user/one',
      component: 'pages/user/one.vue'
    },
    // 携带动态参数 id
    {
      name: 'search-id',
      path: '/search/:id?',
      component: 'pages/search/_id.vue'
    }, 
    // 动态路由路径 slug
    {
      name: 'slug',
      path: '/:slug',
      component: 'pages/_slug/index.vue'
    },
    {
      name: 'slug-comments',
      path: '/:slug/comments',
      component: 'pages/_slug/comments.vue'
    }
  ]
}

对nuxtjs自带路由,在做路由导航守卫时可以结合 中间件(middleware)和插件(plugins):在项目store中提供两个方法 setTokengetToken用来缓存本地token和获取本地缓存的token,用户登录后从后台拿到token信息再通过setToken方法本地缓存token (服务端不能使用localStorage和cookie,要安装相关插件 cookie-universal-nuxt,并在nuxt.config.js进行配置 modules: ['cookie-universal-nuxt'] 就可以使用 this.$cookies.set()this.$cookies.get() 等方法做本地缓存)。当本地页面刷新时候,在生命周期 middleware 阶段获取本地token,如果获取失败就直接返回登录页面,如果获取成功则会继续在 plugins 中的router.js 中进行路由守卫的配置,此时类似 vue 中路由导航守卫配置。具体的代码如下:

// store/index.js
export const state = {
    token: ''
}
export const mutations = {
    setToken(state,token){
        state.token = token;
        this.$cookies.set('token',token);
    },
    getToken(state){
        state.token = this.$cookies.get('token');
    }
}
// middleware/auth.js
export default ({store,route,redirect,params,query,req,res})=>{
    store.commit('getToken');
    if( !store.state.token ){
        redirect('/login');
    }
}
// plugins/router.js
export default ({app})=>{
    app.router.beforeEach((to,from,next)=>{
        console.log( to );
        next();
    })
}
二、使用 @nuxtjs/router 插件配置路由

下载安装插件 @nuxtjs/router:npm i @nuxtjs/router -S;完成后在nuxt.config.js 的 modules模块进行配置

modules: [
    '@nuxtjs/router'
]

把router.js文件放入nuxt项目根目录,当启用当前的路由方式的时候,当前的 router.js 文件会覆盖nuxtjs自动生成的router.js 文件,此时原来的nuxtjs 自带的路由方式失效。如下配置router.js 文件:

import Vue from "vue";
import VueRouter from "vue-router";
import Home from "@/pages/Home.vue";
import About from "@/pages/About.vue";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "Home",
    component: Home,
  },
  {
    path: "/about",
    name: "About",
    component:About
  },
];

export function createRouter() {
  return new VueRouter({
    mode: 'history',
    routes
  })
}

五、模块 modules 和 axios 配置

用 Nuxt 开发应用程序时,很快会发现框架的核心功能还不够;另一方面,开箱即用支持每个项目的需求将使 Nuxt 非常复杂且难以使用。这就是 Nuxt 提供更高阶模块系统的原因,可以轻松扩展核心。 模块只是在引导 Nuxt 时按顺序调用的函数。 框架在加载之前等待每个模块完成。 例如我们如果在项目构建之初没有选择 axios 可以在后续安装和配置:

// nuxt.config.js
modules: [
    '@nuxtjs/axios'
]
  • SSR使用Axios: 服务器端获取并渲染数据,asyncData 方法在渲染组件之前异步获取数据,并把获取的数据返回给当前组件。
export default {
  async asyncData({app}) {
    let data = await app.$axios.get("/list")
    return {
      list: data
    };
  },
  data() {
    return {
      list: []
    }
  }
}
  • 非SSR使用Axios: 这种使用方式就和我们平常一样,访问 this 进行调用
export default {
  data() {
    return {
      list: []
    }
  },
  async created() {
    let data = await this.$axios.get("/list")
    this.list = data
  },
}
  • 自定义配置Axios: 通常情况下,我们都需要对 axios 做自定义配置(baseUrl、拦截器),这时可以通过配置 plugins 来引入,定义插件 /plugins/axios.js:
export default function({ app: { $axios } }) {
  $axios.defaults.baseURL = 'http://127.0.0.1:3000'
  $axios.interceptors.request.use(config => {
    return config
  })
  $axios.interceptors.response.use(response => {
    if (/^[4|5]/.test(response.status)) {
      return Promise.reject(response.statusText)
    }
    return response.data
  })
}
  • axios 配置代理:解决跨域问题需要配置代理,需要安装 npm install @nuxtjs/proxy -S
modules: [
    '@nuxtjs/axios',
    '@nuxtjs/proxy'
],
axios:{
    proxy:true, // 是否可以跨域
    baseUr: process.env._ENV == 'production'? 'xxx' ? 'xxx'
},
proxy:{
    '/api':{
          target:'http://localhost:4000',
          pathRewrite:{
            '^/api':'',
          }
    }
},