我之前的一篇文章,《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 (Pressto select, to toggle all, to invert selection)
8 Choose linting tools (Pressto select, to toggle all, to invert selection)
9 Choose test framework None
10 Choose rendering mode Universal (SSR)
11 Choose development tools (Pressto 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官网都有,我多此一举多安装一遍而已
安装完之后--项目目录如下:
├── 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.config.js中plugins注册一下
plugins: [
{
src: '~/plugins/main',
ssr: true
}
],
然后我们来看.vue文件中,接口的使用和调用
asyncData
{{item.exam_name}}
{{item.exam_enroll}}
{{item.exam_enroll}}
{{item.exam_result}}
{{item.exam_result}}
{{item.exam_face}}
{{item.exam_face}}
课程推荐
{{item.city_names}}
{{item.cate_name}}
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优化,除非是这个组件的内容是不准备让爬虫爬取的
这是官方的解释---仅限于页面组件
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;
}
这里有个地方需要注意一下,在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,里面的路由已经自动生成,文件路径和路由如下图
然后在_cid.vue中随便写几个字,打开/exam/2340/0就可以看到了,文件配置完成
这里要注意下,this.$route.name获取的路由名称不再是exam,而是exam-id-cid
路由页面获取参数
这里做的功能是城市和类型选择,遍历拼接的地址
在_cid.vue中,路由的跳转,在nuxt中是使用
本地的项目是如下图
上面的路由跳转是:to=""拼接的路由路径,参数就是接口的id,cid。id是地区的标志,cid是类型的标志
咱们做的项目优化nuxt的是以seo优化为前提的,所以要使用asyncData()来渲染dom和head(),但是我们前面知道asyncData()是不能获取data中的参数的,
但是如果是路由传参,这里可以是可以接受参数的,如下图
用上图的这种写法,传入参数{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,结构如下:
我们看到项目中多了一个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结构
然后如果有成百上千路由的话,可以和后台配合合作,大致就是后台通过接口给你返回所有的可配置的路由,你配置到generate里面,然后就可以了,这块我项目还没做到,过两天再更新
下面来说一下动态路由打包的利弊
上面说过,nuxt.config.js文件中配置了generate属性,打包上线的时候,会根据这个配置生成静态文件,从而实现seo的dom和head渲染,这种方法是与后台接口配合,大致就是,接口提供给你要做的所有的需要seo优化的页面的路由,通过generate配置,打包的时候请求接口,返回路由路径,生成静态文件,如下图
然后这个接口返回的路由数据,如下图
这样npm run generate打包之后,会生成dist文件夹,dist文件夹格式如下图
这样发布到线上,打开页面右键---查看源代码,就可以看到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持续更新