nuxt基于vuejs的seo动静态优化

我之前的一篇文章,《Vue-cli3基于webpack + prerender-spa-plugin + vue-meta-info的seo优化》,是对静态页面的seo处理,但是也存在很多弊端。比如上线之后浏览器右键查看源代码,会看不到dom结构,比如动态加载head中的meta和title源码也会显示undefined。原因是因为浏览器处理的时候,dom还没有加载完毕。相信下面的nuxt看完之后你会放弃上面这篇文章

本文涉及的主要有,nuxt框架的初始化搭建,局域网访问配置,全局axios配置(包括asyncData(),接口请求),swiper插件使用,引入字体包,动态路由--参数配置---generate打包,cross-env开发和线上环境配置,nuxt并不是最终的优化方案

本文内容较长,没有必要全部看完,可以只看需要的,还有下面的配置信息,第二次做的时候,配置起来有少许的出入,so懂理论就好了

背景

2016 年 10 月 25 日,zeit.co 背后的团队对外发布了 Next.js,一个 React 的服务端渲染应用框架。几小时后,与 Next.js 异曲同工,一个基于 Vue.js 的服务端渲染应用框架应运而生,我们称之为:Nuxt.js

定义

Nuxt.js 是一个基于 Vue.js 的通用应用框架。为基于 Vue.js 的应用提供生成对应的静态站点的功能。通过asyncData钩子函数可以生成利于seo优化的动静态站点

搭建

Nuxt.js团队创建了脚手架工具 create-nuxt-app。

确保安装了npx(npx在NPM版本5.2.0默认安装了):
安装之前请先检查一下npm的版本

$ npx create-nuxt-app <项目名>

或者用yarn :

$ yarn create nuxt-app <项目名>

npx create-nuxt-app nuxt1
npx: 341 安装成功,用时 115.532 秒
create-nuxt-app v2.10.1
✨ Generating Nuxt.js project in nuxt1
1 Project name nuxt1
2 Project description My doozie Nuxt.js project
3 Author name smook
4 Choose the package manager Npm
5 Choose UI framework None
6 Choose custom server framework None (Recommended)
7 Choose Nuxt.js modules (Press to select, to toggle all, to invert selection)
8 Choose linting tools (Press to select,
to toggle all, to invert selection)
9 Choose test framework None
10 Choose rendering mode Universal (SSR)
11 Choose development tools (Press to select,
to toggle all, to invert selection)
� Successfully created project nuxt1

To get started:

    cd nuxt1
    npm run dev

To build & start for production:

    cd nuxt1
    npm run build
    npm run start

10步骤这里我的经验是选SSR,因为我本身就是奔着seo优化来的,所以SSR服务端渲染这块应该是这个

-上面是一整个安装流程-nuxt官网都有,我多此一举多安装一遍而已

安装完之后--项目目录如下:


image.png

├── assets // 资源文件。用于组织未编译的静态资源入LESS、SASS 或 JavaScript

├── components // 组件。用于自己编写的Vue组件,比如滚动组件,日历组件,分页组件
│ └── logo.vue // 默认logo组件

├── layouts // 布局。页面都需要有一个布局,默认为 default。它规定了一个页面如何布局页面。所有页面都会加载在布局页面中的 标签中。
│ └── default.vue // 默认模板页面,类似mvc中的layout

├── middleware // 中间件。存放中间件。可以在页面中调用: middleware: 'middlewareName'

├── pages   // 页面。一个 vue 文件即为一个页面。也会根据pages里面的文件结构自动生成路由
│ └── index.vue // 默认首页面

├── plugins // 用于存放JavaScript插件的地方,或者一些必要的配置,全局配置

├── static // 用于存放静态资源文件,比如图片,此类文件不会被 Nuxt.js 调用 Webpack 进行构建编译处理。 服务器启动的时候,该目录下的文件会映射至应用的根路径 / 下。

├── store   // 用于组织应用的Vuex 状态管理。

├── .editorconfig // 开发工具格式配置

├── .eslintrc.js // ESLint的配置文件,用于检查代码格式

├── .gitignore // 配置git不上传的文件

├── nuxt.config.js // 用于组织Nuxt.js应用的个性化配置,比如网站title,已便覆盖默认配置

├── package.json // npm包管理配置文件

└── README.md // 说明文档

上面的结构是整个新建项目的目录图,和我网上找的和自己总结的目录中每个的作用

现在我们运行下面的代码可以进行项目初体验

cd nuxt1
npm run dev

然后好像默认的是localhost:3000,在浏览器打开就可以了

修改局域网配置

使用localhost:3000访问之后,有疑问的同学会发现,你发给在同一个局域网的同事,他们是打不开你的项目的,跟vue-cli创建的项目不一样!其实这里可以通过修改一下config来修改,具体配置如下:

在package.json中新添加

这个就是你本地的地址,同一局域网是访问不到
"config": {
    "nuxt": {
      "host": "127.0.0.1",
      "port": "8080"
    }
  }

这个是0.0.0.0配置之后,重新运行,同一局域网可以访问
"config": {
    "nuxt": {
      "host": "0.0.0.0",
      "port": "8080"
    }
  }

具体的效果,可以本地测试一下

全局axios配置

个人见解

根据前面安装的步骤,已经安装了axios,或者可以去package.json中去检查一下

npm install axios --save

安装好之后在/plugins目录下新建baseUrl.js

// 这里是一个默认的url,可以没有
let baseUrl = ''

// window对象要在.vue文件的mounted生命周期之后才可以获取,mounted之前就不行
// let hostnames = location.hostname   
// process
switch (process.env.NODE_ENV) {
  case 'development':
    // 开发环境url
    baseUrl = 'https://开发域名'
    break
  case 'production':
    // 生产环境url
    baseUrl = 'https://生产域名'
    break
}

export default baseUrl

然后再在/plugins目录下新建axios.js

import * as axios from 'axios'
import qs from 'qs'
import baseUrl from './baseUrl'

axios.defaults.baseURL = baseUrl
axios.defaults.timeout = 20000
// 默认是否允许携带cookie
axios.defaults.withCredentials = true
axios.defaults.headers = {
  'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
}

axios.interceptors.request.use(
  config => {
    // 若是有做鉴权token , 就给头部带上token
    // var token = localStorage.getItem('token')
    // if (token !== null) {
    //   if (token !== '') {
    //     config.headers.token = token
    //   }
    // }
    // store.dispatch('showLoading', '加载中')
    return config
  },
  error => {
    // store.dispatch('hideLoading')
    return Promise.reject(error)
  }
)

axios.interceptors.response.use(
  response => {
    // let getVisitor = localStorage.getItem('getVisitor')
    // if (getVisitor === '游客') {
    //   if (response.data.code === 400) {
    //     return Promise.reject({
    //       message: response.data.msg
    //     })
    //   }
    // } else if (getVisitor === null) {
    //   if (response.data.code === 400) {
    //     router.push({
    //       name: 'auth'
    //     })
    //     return Promise.reject({
    //       message: response.data.msg
    //     })
    //   }
    // }
    return response
  },
  error => {
    if (!error.response) {
      return Promise.reject ({
        message: '请求无响应'
      })
    }
    let message
    switch (error.response.status) {
      case 400:
        message = '错误请求'
        break
      case 401:
        message = '未授权'
        break
      case 403:
        message = '拒绝访问'
        break
      case 404:
        message = '请求路径未找到'
        break
      case 500:
        message = '服务器异常'
        break
      default:
        message = '未知错误'
    }
    return Promise.reject(error)
  }
)

const http = {
  get (url, payload = undefined) {
    return axios({
      method: 'get',
      url: url,
      params: payload,
      paramsSerializer: params => {
        return qs.stringify(params, { indices: false })
      }
    })
  },
  post (url, payload = undefined) {
    return axios({
      method: 'post',
      url: url,
      data: payload,
      transformRequest: [
        data => {
          return qs.stringify(data, { indices: false })
        }
      ]
    })
  },
  put (url, payload = undefined) {
    return axios({
      method: 'post',
      url: url,
      data: payload
    })
  },
  delete (url, payload = undefined) {
    return axios({
      method: 'delete',
      url: url,
      data: payload
    })
  },
  postJson (url, payload = undefined) {
    return axios({
      method: 'post',
      url: url,
      data: payload,
      headers: {
        'Content-Type': 'application/json;charset=UTF-8'
      }
    })
  },
  postFile (url, payload = undefined) {
    return axios({
      method: 'post',
      url: url,
      data: payload,
      headers: {
        'Content-Type': 'multipart/form-data'
      }
    })
  },
  all (promises) {
    return Promise.all(promises)
  },
  getBase () {
    return baseUrl
  }
}

export default http

这里还需要安装qs

npm install qs --save

然后再在/plugins目录下新建main.js


// 设置全局变量
import Vue from 'vue' // vue 文件引入 - 方便在vue方法内容直接 this 调取
import http from './axios'

// 全局http
let mainHttp = {
    install(Vue){
        Vue.prototype.$http = http  // 变量的内容 后期可以在vue中 this->$api.xxx 使用
    }
}

Vue.use(mainHttp)
 
// 这里是 为了在 asyncData 方法中使用
export default ({ app }, inject) => {
    // Set the function directly on the context.app object
    app.$http = http
}

现在配置完了,看一下/plugins目录的文件结构


nuxt基于vuejs的seo动静态优化_第2张图片
image.png

然后在nuxt.config.js中plugins注册一下

plugins: [
    {
      src: '~/plugins/main',
      ssr: true
    }
  ],

然后我们来看.vue文件中,接口的使用和调用
asyncData



export default{
  async asyncData (params) {
  //  这里通过params.app 就可以找到我们注册的全局$http
 //  因为asyncData方法里面不能读取和使用this关键字,所以我们只能使用
 //  params这个全局属性来获取全局变量
    // console.log(params.app.$http)
    let { data } = await params.app.$http.post('/course/getExamLists', ({
      is_top: 1,
      page: 1,
      page_count: 6
    }))
    console.log(data.data.length)
    return { testList: data.data }
  },
}

//我测试的是接口这里的传参的参数,我没办法从data()中获取,
//所以我是写死的,然后就是我这里的接口和下面正常接口是一起加载的,
//当然这个asyncData是在dom渲染之前就已经加载了,
//然后这里的testList也不用在data()中定义,因为它运行的时候,data()压根就还没开始运行
//如果不写这个asyncData函数的话,项目在浏览器运行的时候,右键查看源代码,压根源代码里就没有这个接口渲染的dom节点,所以这个方法是必然
//而下面的正常接口方法,是实现功能的正常接口,asyncData在dom渲染之前就执行了,
//所以后续的逻辑操作根绝压根用不到它,我个人理解,他的作用就是为了渲染源码dom
//特指接口渲染的dom

正常的接口调用

methods: {
    Course () {
      // this.$qs.stringify
      //  这里接口调用只用this.$http就可以随意使用post或者get等等
      this.$http.post('/course/getExamLists', {
        is_top: 1,
        page: this.page,
        page_count: this.page_count
      }).then((res) => {
        this.testList = res.data.data
        this.allNum = res.data.all_num
        if (this.allNum > 0) {
          this.allNum = Math.ceil(Number(this.allNum) / Number(this.page_count))
        }
        this.testList.map((r, index) => {
          for (const key in this.list[0]) {
            if (Number(key) === Number(r.city_id)) {
              this.testList[index].city_names = this.list[0][key]
              this.testList[index].key = 'sx' + key
            }
            if (Number(key) === Number(r.province_id) && Number(r.city_id) === 0) {
              this.testList[index].city_names = this.list[0][key]
              this.testList[index].key = 'sx' + key
            }
          }
        })
      })
    },
}

在这里要提一句,上面这种方式,前提是在你设置了同一局域网可以访问的那个配置之后才可以的,不然会报错的,错误好像是什么127.0.0.1:80,所以上面文章不仔细看的,到这里运行报错我只能说,啊,好爽,走我的老路

还有就是子组件,在子组件中是不可以使用asyncData函数的,因为子组件搭建的时候是放在components目录下面的,而不是pages目录下,所以子组件不可以使用,但是我们可以在父组件中使用asyncData函数,然后通过props函数接收。但是这种有一个问题就是通过props渲染子组件,浏览器查看源代码的时候是没有渲染的这一块的dom结构的,所以不利于seo优化,除非是这个组件的内容是不准备让爬虫爬取的

这是官方的解释---仅限于页面组件


nuxt基于vuejs的seo动静态优化_第3张图片
image.png

swiper组件安装

因为nuxt里面window对象必须在mounted生命周期之后使用,所以这里使用的轮播插件是vue-awesome-swiper

安装

npm install vue-awesome-swiper --save

安装完之后在/plugins目录下新建swiper.js--文件名字随意

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

Vue.use(VueAwesomeSwiper)

然后再在nuxt.config.js中注册一下

module.exports = {
  // some nuxt config...
  plugins: [
    { src: '~/plugins/swiper.js', ssr: false },
  ],
  // some nuxt config...
  css: [
    'swiper/dist/css/swiper.css'
  ],
  // some nuxt config...
}
这里的ssr要设置成false,如果为true的话可能会报window is undefined类似的错误

然后就是在.vue的文件中写轮播了





//这里的样式可能不太对,所以不要太在意,我这里写是为了说明一个原因
//是style中的scoped要去掉,如果要自定义按钮的话
//我这里的.my-bullet和.my-bullet-active是自定义的按钮,如果scoped不去掉的话,是没办法在轮播中显示的!唯一的遗憾

问题1:

有时候点击分页器,轮播就不能继续了,根本原因是是少了一个属性

autoplay: {
  delay: 5000,
//加上这句就好了
  disableOnInteraction: false
},

asyncData一次请求多个接口怎么办,asyncData多并发请求如下:

async asyncData (ctx) {
    let [data, data1, data2, data3] = await Promise.all([
      ctx.app.$http.post('/index/wwwIndex'),
      ctx.app.$http.post('/course/getExamLists', {
        is_top: 1,
        page: 1,
        page_count: 6
      }),
      ctx.app.$http.post('/news/newsLists', {
        news_type: 1,
        is_top: 1
      })
    ])
    // console.log(data2.data)
    // testList: data.data,
    return {
      banner: data.data.data.pc_www_top_banner.img_lists,
      testList: data1.data.data,
      newList: data2.data.data,
      teacher_lists: data.data.data.teacher_lists,
      knowList: data.data.data.link_article_list
    }

swiper插件也可以单纯的使用swiper组件,这个不需要上面swiper的Vue.user(swiper)注入,
只需要在nuxt.config.js中把css注入就好了,然后在.vue文件中直接import swiper from 'swiper'
就可以正常使用swiper了,我写这篇文章的页面有很多轮播,就不多做介绍了,自己摸索,都差不多

引入字体包

在/assets目录下新建 font文件夹把字体文件放进去,同事新建一个css/scss文件font
font.scss

@font-face {
  font-family: 'DIN Alternate Bold';
  src: url('DINAlternateBold.ttf');
  font-weight: normal;
  font-style: normal;
}

然后在nuxt.config.js中注入路径

css: [
    '@/assets/font/font.scss'
]

这样在.vue文件中,就可以使用了

.foot_right .top1 li p{
  font-size: 20px;
  font-family:'DIN Alternate Bold';
  margin-bottom: 4px;
}
nuxt基于vuejs的seo动静态优化_第4张图片
image.png

这里有个地方需要注意一下,在vue-cli脚手架下面,font文件夹下面的字体文件名字是可以识别空格的,例如'DIN Alternate Bold.ttf。但是在nuxt中要重命名,把空格去掉,不然会一直报错

路由配置传参(动态路由)--以及generate打包

这个路由配置比较麻烦一点,由于不熟悉花了将近2天的时间来搞这个

前面介绍了/pages目录的设计会自动生成路由,去.nuxt目录下的route.js就可以看到,今天要说的就是参数的传递(动态路由)和generate打包生成静态页面

以我项目为例
http://192.168.0.150:8080/exam/2340/0这是本地的链接,/exam/2340/0其中的2340和0是传入的参数id和cid,/exam/是要跳转的路由

配置如下

就是在/pages目录下新建exam文件夹,然后在exam中新建_id文件夹,然后在_id文件夹中新建_cid.vue文件,保存之后可以去/.nuxt目录下打开route.js,里面的路由已经自动生成,文件路径和路由如下图

image.png

nuxt基于vuejs的seo动静态优化_第5张图片
image.png

然后在_cid.vue中随便写几个字,打开/exam/2340/0就可以看到了,文件配置完成

这里要注意下,this.$route.name获取的路由名称不再是exam,而是exam-id-cid

路由页面获取参数

这里做的功能是城市和类型选择,遍历拼接的地址

在_cid.vue中,路由的跳转,在nuxt中是使用,用法跟一样
本地的项目是如下图


nuxt基于vuejs的seo动静态优化_第6张图片
image.png

nuxt基于vuejs的seo动静态优化_第7张图片
image.png

上面的路由跳转是:to=""拼接的路由路径,参数就是接口的id,cid。id是地区的标志,cid是类型的标志

咱们做的项目优化nuxt的是以seo优化为前提的,所以要使用asyncData()来渲染dom和head(),但是我们前面知道asyncData()是不能获取data中的参数的,

但是如果是路由传参,这里可以是可以接受参数的,如下图

nuxt基于vuejs的seo动静态优化_第8张图片
image.png

用上图的这种写法,传入参数{app, params},app这里是前面ctx下面的app结构。params就是要接受的参数
使用console.log(params)打印可以看到浏览器控制台打印出{id: 2340, cid: 0},这样前面遍历的地区就可以获取参数了,这样的话,我不管是点击地区或者类型,参数都会传过来,这样浏览器查看源代码就可以看到渲染了dom。

开发中遇到一个问题,就是刷新浏览器的时候报错,request to....500,记不太清楚了!这个错误的原因是asyncData()方法在项目运行的时候,请求到的接口有问题,跟前端无关,跟前端无关,跟前端无关,说三遍。不要傻乎乎的在前端配置文件或者代码中找错误,是接口的问题!把asyncData()方法注释,在methods钩子函数中走一遍方法,F12,network中查看接口请求,就可以看到接口报错了!这时候通知后台修改接口!

generate打包

动态传参完成之后,我们要验证我们之前写的asyncData()渲染的dom是否发布到线上也能正常渲染

//运行
npm run generate

预期的打包之后的结构是exam->下面有很多id子文件夹,id子文件夹里面有cid文件夹,cid文件夹里面是index.html,结构如下:


nuxt基于vuejs的seo动静态优化_第9张图片
image.png

我们看到项目中多了一个dist文件夹,。这就是generate之后的打包的文件,但是我们在项目中真正看到的结果并不是这个样子的,并没有exam文件夹,或者/exam里面一个exam.vue

不能显示的真正原因是因为没有配置generate属性

在nuxt.config.js中设置generate属性配置

我们这里先写死这几个
export default {
  generate: {
    routes: [
      '/exam/2340/0',
      '/exam/2341/0',
      '/exam/2352/0'
    ]
  }
}

上面设置了这三条之后,再运行npm run generate . 生成的dist文件夹中的路径,就跟我前面的那张图结构一样了,这样你把dist文件夹内容放到线上,这三个路由切换的时候,右键查看源代码就可以看到渲染的dom了,下图是线上查看源代码之后的dom结构

nuxt基于vuejs的seo动静态优化_第10张图片
image.png

然后如果有成百上千路由的话,可以和后台配合合作,大致就是后台通过接口给你返回所有的可配置的路由,你配置到generate里面,然后就可以了,这块我项目还没做到,过两天再更新

下面来说一下动态路由打包的利弊

上面说过,nuxt.config.js文件中配置了generate属性,打包上线的时候,会根据这个配置生成静态文件,从而实现seo的dom和head渲染,这种方法是与后台接口配合,大致就是,接口提供给你要做的所有的需要seo优化的页面的路由,通过generate配置,打包的时候请求接口,返回路由路径,生成静态文件,如下图


nuxt基于vuejs的seo动静态优化_第11张图片
image.png

然后这个接口返回的路由数据,如下图


nuxt基于vuejs的seo动静态优化_第12张图片
image.png

这样npm run generate打包之后,会生成dist文件夹,dist文件夹格式如下图


nuxt基于vuejs的seo动静态优化_第13张图片
image.png

nuxt基于vuejs的seo动静态优化_第14张图片
image.png

这样发布到线上,打开页面右键---查看源代码,就可以看到dom结构和head了。

开发(本地)和生产(线上)的环境配置

上面看到我的generate配置里面使用了process.env.BASE_URL,这个其实是配置了开发和生产环境的部署,需要安装一个依赖cross-env

npm install cross-env --save

安装完成之后先去package.json中配置一下scripts属性

"scripts": {
    "dev": "cross-env BASE_URL=https://测试.cn NODE_ENV=development nuxt",
    "test": "cross-env BASE_URL=https://测试.cn NODE_ENV=production nuxt generate",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "cross-env BASE_URL=https://生产.cn NODE_ENV=production nuxt generate"
  },

最前面配置的全局axios中/plugins/axios.js要去掉一些语句和修改下

注释掉baseUrl的加载和默认baseURL配置
import * as axios from 'axios'
import qs from 'qs'
// import baseUrl from './baseUrl'

// axios.defaults.baseURL = baseUrl
axios.defaults.timeout = 20000
// 默认是否允许携带cookie
axios.defaults.withCredentials = true
axios.defaults.headers = {
  'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
}

然后axios.js后面的请求方法中添加上process.env.BASE_URL

get (url, payload = undefined) {
    return axios({
      method: 'get',
      url: process.env.BASE_URL + url,
      params: payload,
      paramsSerializer: params => {
        return qs.stringify(params, { indices: false })
      }
    })
  },
  post (url, payload = undefined) {
    return axios({
      method: 'post',
      url: process.env.BASE_URL + url,
      data: payload,
      transformRequest: [
        data => {
          return qs.stringify(data, { indices: false })
        }
      ]
    })
  },

然后再在nuxt.config.js中添加一下env配置

export default {
  mode: 'universal',
  env: {
    BASE_URL: process.env.BASE_URL,
    NODE_ENV: process.env.NODE_ENV
  }
}

这样就好了,.vue文件中的请求接口方式不变,这样项目运行或者打包的话,就可以把package.json中的配置的cross-env进行全局适配,要知道nuxt中配置文件中window和location.name是访问不到的,所以这种办法是很方便的!

我上面配置的不全,感兴趣的可以去搜搜别的攻略
使用npm run dev这是运行的本地(开发)环境,接口域名对应的是测试接口
使用npm run test这是打包测试的,然后发布到测试环境,同样的接口也是使用的测试接口---注意后面的nuxt generate
使用npm run generate这是打包正式的,然后发布到测试,测试没问题之后,再发布到正式,接口域名是正式(线上)接口

并不是最终的优化方案

但是上面这个也有一个弊端,并不能做到真正的动态化!你想,如果我的后台操作系统,添加了一篇文章,那么在前台显示的时候,这篇文章是一个新的路由/路径,那么这个新增的文章,后台添加完成之后,前台是看不到seo的效果的,要再重新打包一次,发布线上才可以,还有就是如果文件,和路径少了还好说,如果有上千,上万,几百万的时候,生成的静态文件我觉得服务器会爆炸,所以我的建议如果要使用nuxt,就要考虑你是否是要真正的实现动态化seo优化。看了b站的,还有掘金的,目前还没搞懂他们的技术点。所以建议是服务端渲染,通过后台或者node渲染生成html,渲染到前台!

后续配置和操作优化seo持续更新

你可能感兴趣的:(nuxt基于vuejs的seo动静态优化)