vue的seo
我们知道,常规用 Vue/React 开发的是 SPA 应用。
但是天然的单页面应用 SEO 就是不好。
虽然说现在也有各种技术可以改善了,比如使用服务端渲染、静态页面生成,不过也存在各种缺点。
但是即使这样,也抵不住 Vue/React 这类框架的潮流。
也有很多产品也可以通过其他亮点而不依赖 SEO 普及开,
也有需要登录才能用的,使用 SEO 也没有什么意义。
几个名词
CSR: Client Side Render 客户端渲染,即普通的SPA
SSR: Server Side Render 服务端渲染
SSG: Static Site Generation 静态生成, 也有人称其为预渲染Prerendering
这个在使用此类框架创建项目的时候,会让你选择。比如nuxtJS
? Rendering mode: // 选择你想要的 Nuxt 模式
Universal (SSR / SSG) // 通用模式 ssr或者ssg
Single Page App // 普通spa模式
SSR vs SSG
如果你调研服务器端渲染 (SSR) 只是用来改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染。无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。
SSG 性能最佳,SSR 适用范围最广,CSR 跳转体验最优,非得在三者之间做出抉择吗?不,我全都要
CSR模式
这是Vue/React默认的模式。在这种模式下,页面之间跳转体验良好,借助于vdom、historyApi等技术,让网站交互如原生app般流畅。 但是却对seo不好,尤其是国内搜索引擎,如百度爬虫。
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
<title>csr-vue</title>
<link href="/js/about.addee37b.js" rel="prefetch" />
<link href="/css/app.b087a504.css" rel="preload" as="style" />
<link href="/js/app.a33b1734.js" rel="preload" as="script" />
<link href="/js/chunk-vendors.2d69e2a7.js" rel="preload" as="script" />
<link href="/css/app.b087a504.css" rel="stylesheet" />
</head>
<body>
<noscript>
<strong>
We're sorry but csr-vue doesn't work properly without JavaScript enabled.
Please enable it to continue.
</strong>
</noscript>
<div id="app">
</div>
<script src="/js/chunk-vendors.2d69e2a7.js">
</script>
<script src="/js/app.a33b1734.js">
</script>
</body>
</html>
可以看到,客户端渲染是通过加载执行JS来创建DOM元素构建页面,但是爬虫只是请求静态资源,不会执行JS文件,所以抓取不到DOM结构,也分析不出来有用的信息。
SSR模式
ssr和ssg均有原生的编写模式,如vue的vue-server-renderer、react的react-dom/server。但是编写起来较为麻烦,除了研究外,实际用于生产的较少,作为研究原理性质可用。如果后边有时间 会单拉出来讲
现在我们使用框架来解决,vue对应的为nuxtJs(对应的react的框架为nextjs)。
用脚手架创建项目完毕npx create-nuxt-app ssr-vue,
并且本地运行启动npx create-nuxt-app ssr-vue
<!doctype html>
<html data-n-head-ssr lang="en" data-n-head="%7B%22lang%22:%7B%22ssr%22:%22en%22%7D%7D">
<head>
<title>ssr-vue</title>
<meta data-n-head="ssr" charset="utf-8">
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
<meta data-n-head="ssr" data-hid="description" name="description" content="">
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="preload" href="/_nuxt/runtime.js" as="script">
<link rel="preload" href="/_nuxt/commons/app.js" as="script">
<link rel="preload" href="/_nuxt/vendors/app.js" as="script">
<link rel="preload" href="/_nuxt/app.js" as="script">
<link rel="preload" href="/_nuxt/pages/index.js" as="script">
<link rel="preload" href="/_nuxt/components/logo.js" as="script">
<style data-vue-ssr-id="3191d5ad:0">
.nuxt-progress {
position: fixed;
top: 0px;
...
</style>
</head>
<body>
<div data-server-rendered="true" id="__nuxt">
<!---->
<!---->
<div id="__layout">
<div>
<div class="container">
<div>
<svg>...</svg>
<h1 class="title">
ssr-vue
</h1>
<div class="links"><a href="https://nuxtjs.org/" target="_blank" rel="noopener noreferrer" class="button--green">
Documentation
</a> <a href="https://github.com/nuxt/nuxt.js" target="_blank" rel="noopener noreferrer" class="button--grey">
GitHub
</a></div>
</div>
</div>
</div>
</div>
</div>
<script>
window.__NUXT__ = (function(a, b) {
return {
layout: "default",
data: [{}],
fetch: {},
error: a,
serverRendered: true,
routePath: b,
config: {
app: {
basePath: b,
assetsPath: "\u002F_nuxt\u002F",
cdnURL: a
}
},
logs: []
}
}(null, "\u002F"));
</script>
<script src="/_nuxt/runtime.js" defer></script>
<script src="/_nuxt/pages/index.js" defer></script>
<script src="/_nuxt/components/logo.js" defer></script>
<script src="/_nuxt/commons/app.js" defer></script>
<script src="/_nuxt/vendors/app.js" defer></script>
<script src="/_nuxt/app.js" defer></script>
</body>
</html>
可以看到dom已经在服务端渲染完毕
我们不用官方脚手架生成的页面,下边我们自己写一个demo,包含请求
<template>
<div class="container">
<fieldset>
<legend>ssr-asyncData数据</legend>
<div v-for="item in teachers" :key="item.id">姓名: {{item.name}}</div>
</fieldset>
<fieldset>
<legend>ssr-data数据</legend>
<div v-for="item in teachers1" :key="item.id">姓名: {{item.name}}</div>
</fieldset>
<fieldset>
<legend>ssr-created</legend>
<div v-for="item in teachers2" :key="item.id">姓名: {{item.name}}</div>
</fieldset>
<fieldset>
<legend>csr-mounted数据</legend>
<div v-for="item in teachers3" :key="item.id">姓名: {{item.name}}</div>
</fieldset>
</div>
</template>
<script>
const getTeachers = ()=> {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
const teachers = [
{id:202101,name:'张老师'},
{id:202102,name:'丁老师'}
];
resolve(teachers);
},2000)
})
}
export default {
data(){ // 会走ssr
return {
teachers1: [
{id:202101,name:'张老师1'},
{ id:202102,name:'丁老师1'}
],
teachers2: [],
teachers3: []
}
},
mounted(){ // 会走csr
this.teachers3 = [
{id:202101,name:'张老师3'},
{id:202102,name:'丁老师3'}
]
},
created(){ // 会走ssr
this.teachers2 = [
{id:202101,name:'张老师2'},
{id:202102,name:'丁老师2'}
]
},
async asyncData({ params }) { // 会走ssr
const teachers = await getTeachers();
return {teachers}
}
}
</script>
<style>
.container {
margin: 0 auto;
padding: 10px;
}
</style>
编译后,可以看到data和created、asyncData里的代码都会被ssr。而其他钩子里的数据都会被csr。
这是因为:
在任何 Vue 组件的生命周期内, 只有
beforeCreate
和created
这两个方法会在 客户端和服务端被调用。其他生命周期函数仅在客户端被调用。
https://www.nuxtjs.cn/guide/plugins
Nuxt.js 会将
asyncData
返回的数据融合组件data
方法返回的数据一并返回给当前组件。
https://www.nuxtjs.cn/guide/async-data
<!DOCTYPE html>
<html data-n-head-ssr="" lang="en" data-n-head="%7B%22lang%22:%7B%22ssr%22:%22en%22%7D%7D">
<head>
<title>ssr-vue</title>
<meta data-n-head="ssr" charset="utf-8" />
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1" />
<meta data-n-head="ssr" data-hid="description" name="description" content="" />
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="preload" href="/_nuxt/runtime.js" as="script" />
<link rel="preload" href="/_nuxt/commons/app.js" as="script" />
<link rel="preload" href="/_nuxt/vendors/app.js" as="script" />
<link rel="preload" href="/_nuxt/app.js" as="script" />
<link rel="preload" href="/_nuxt/pages/index.js" as="script" />
<link rel="preload" href="/_nuxt/pages/index.4ee7ce6db4f1f6ace22f.hot-update.js" as="script" />
<style data-vue-ssr-id="3191d5ad:0">
.nuxt-progress {
position: fixed;
top: 0px;
...
</style>
</head>
<body>
<div data-server-rendered="true" id="__nuxt">
<!---->
<!---->
<div id="__layout">
<div>
<div class="container">
<fieldset>
<legend>ssr-asyncData数据</legend>
<div>
姓名: 张老师
</div>
<div>
姓名: 丁老师
</div>
</fieldset>
<fieldset>
<legend>ssr-data数据</legend>
<div>
姓名: 张老师1
</div>
<div>
姓名: 丁老师1
</div>
</fieldset>
<fieldset>
<legend>ssr-created</legend>
<div>
姓名: 张老师2
</div>
<div>
姓名: 丁老师2
</div>
</fieldset>
<fieldset>
<legend>csr-mounted数据</legend>
</fieldset>
</div>
</div>
</div>
</div>
<script>window.__NUXT__=(function(a,b){return {layout:"default",data:[{teachers:[{id:202101,name:"张老师"},{id:202102,name:"丁老师"}]}],fetch:{},error:a,serverRendered:true,routePath:b,config:{app:{basePath:b,assetsPath:"\u002F_nuxt\u002F",cdnURL:a}},logs:[]}}(null,"\u002F"));</script>
<script src="/_nuxt/runtime.js" defer=""></script>
<script src="/_nuxt/pages/index.js" defer=""></script>
<script src="/_nuxt/pages/index.4ee7ce6db4f1f6ace22f.hot-update.js" defer=""></script>
<script src="/_nuxt/commons/app.js" defer=""></script>
<script src="/_nuxt/vendors/app.js" defer=""></script>
<script src="/_nuxt/app.js" defer=""></script>
</body>
</html>
----再举例子验证-----
<template>
<div class="container">
<span @click="goList">我是详情,点我跳转列表</span>
</div>
</template>
<script>
export default {
methods: {
goList() {
this.$router.push("/");
},
},
mounted(){ // 客户端执行
console.log('mounted');
},
created(){ // 客户端和服务端执行
console.log('created');
},
asyncData(){ // 服务端执行
console.log('asyncData');
}
};
</script>
服务端会打印
浏览器也会打印
nuxtjs路由-入门
和传统的建立路由配置不同,nuxtjs更像小程序和原生html跳转。免路由配置
会依据 pages 目录结构自动生成 vue-router 模块的路由配置。
pages/
--| index.vue
--| detal.vue
index.vue
<template>
<div class="container">
<nuxt-link to="/detail">我是列表,点我跳转详情</nuxt-link>
</div>
</template>
<script>
export default {}
</script>
<style>
.container {
margin: 0 auto;
padding: 10px;
}
</style>
detail.vue
<template>
<div class="container">
<span @click="goList">我是详情,点我跳转列表</span>
</div>
</template>
<script>
export default {
methods: {
goList() {
this.$router.push("/");
},
},
};
</script>
<style>
.container {
margin: 0 auto;
padding: 10px;
}
</style>
nuxtjs视图
Nuxt.js 应用中为指定的路由配置数据和视图,包括应用模板、页面、布局和 HTML 头部等内容。
其他可以参考文档,这里主要演示一个对于seo比较重要的头部信息description、keywords
<template>
<div class="container">
<nuxt-link to="/detail">我是列表,点我跳转详情</nuxt-link>
</div>
</template>
<script>
export default {
head: {
title: "文章列表",
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{
name: "description",
content: "这是一个神奇的网站",
},
{
name: "keywords",
content: "神奇,网站",
},
],
link: [
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css?family=Roboto",
},
],
},
};
</script>
<style>
.container {
margin: 0 auto;
padding: 10px;
}
</style>
编译后代码,可以看到vue-ssr处理后html,已经通过header属性为html加上了标签
<!DOCTYPE html>
<html data-n-head-ssr="" lang="en" data-n-head="%7B%22lang%22:%7B%22ssr%22:%22en%22%7D%7D">
<head>
<title>文章列表</title>
<meta data-n-head="ssr" charset="utf-8" />
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1" />
<meta data-n-head="ssr" data-hid="description" name="description" content="" />
<meta data-n-head="ssr" charset="utf-8" />
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1" />
<meta data-n-head="ssr" name="description" content="这是一个神奇的网站" />
<meta data-n-head="ssr" name="keywords" content="神奇,网站" />
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico" />
<link data-n-head="ssr" rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto" />
<link rel="preload" href="/_nuxt/runtime.js" as="script" />
<link rel="preload" href="/_nuxt/commons/app.js" as="script" />
<link rel="preload" href="/_nuxt/vendors/app.js" as="script" />
<link rel="preload" href="/_nuxt/app.js" as="script" />
<link rel="preload" href="/_nuxt/pages/index.js" as="script" />
<style data-vue-ssr-id="3191d5ad:0"></style>
...
</head>
<body>
...
</body>
</html>
SSG模式
ssg既对seo友好,也对服务器友好。
由于是开发的时候就是已经生成好了的静态资源,所以不需要服务器实时做处理。
同时也不会在发起请求,因为在编译打包的时候,就会访问后端数据,填充到静态页面中
但是也正因为如此,ssg页面常常会存在数据更新不及时,每次更新数据,都需要再次编译方可更新页面的问题,
所以ssg常用在不常更新数据的页面上,比如about、concat等等
举个例子
<template>
<div class="container">
<fieldset>
<legend>ssr-asyncData数据</legend>
<div v-for="item in teachers" :key="item.id">姓名: {{item.name}}</div>
</fieldset>
<fieldset>
<legend>ssr-data数据</legend>
<div v-for="item in teachers1" :key="item.id">姓名: {{item.name}}</div>
</fieldset>
<fieldset>
<legend>ssr-created</legend>
<div v-for="item in teachers2" :key="item.id">姓名: {{item.name}}</div>
</fieldset>
<fieldset>
<legend>csr-mounted数据</legend>
<div v-for="item in teachers3" :key="item.id">姓名: {{item.name}}</div>
</fieldset>
</div>
</template>
<script>
const getTeachers = ()=> {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
const teachers = [
{id:202101,name:'张老师'},
{id:202102,name:'丁老师'}
];
resolve(teachers);
},2000)
})
}
export default {
head: {
title: "文章列表",
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{
name: "description",
content: "这是一个神奇的网站",
},
{
name: "keywords",
content: "神奇,网站",
},
],
link: [
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css?family=Roboto",
},
],
},
data(){ // 会走ssg
return {
teachers1: [
{id:202101,name:'张老师1'},
{ id:202102,name:'丁老师1'}
],
teachers2: [],
teachers3: []
}
},
mounted(){ // 会走csr
this.teachers3 = [
{id:202101,name:'张老师3'},
{id:202102,name:'丁老师3'}
]
},
created(){ // 会走ssg
this.teachers2 = [
{id:202101,name:'张老师2'},
{id:202102,name:'丁老师2'}
]
},
async asyncData({ params }) { // 会走ssg
const teachers = await getTeachers();
return {teachers}
}
};
</script>
<style>
.container {
margin: 0 auto;
padding: 10px;
}
</style>
然后我们运行ssg的命令,生产静态网页
npm run generate
进入dist,启动项目,我用的是http-server
<!DOCTYPE html>
<html data-n-head-ssr="" lang="en" data-n-head="%7B%22lang%22:%7B%22ssr%22:%22en%22%7D%7D">
<head>
<title>文章列表</title>
<meta data-n-head="ssr" charset="utf-8" />
<meta data-n-head="ssr" name="viewport" content="width=device-width,initial-scale=1" />
<meta data-n-head="ssr" data-hid="description" name="description" content="" />
<meta data-n-head="ssr" charset="utf-8" />
<meta data-n-head="ssr" name="viewport" content="width=device-width,initial-scale=1" />
<meta data-n-head="ssr" name="description" content="这是一个神奇的网站" />
<meta data-n-head="ssr" name="keywords" content="神奇,网站" />
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico" />
<link data-n-head="ssr" rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto" />
<link rel="preload" href="/_nuxt/e4e7482.js" as="script" />
<link rel="preload" href="/_nuxt/2a8f614.js" as="script" />
<link rel="preload" href="/_nuxt/699e2f6.js" as="script" />
<link rel="preload" href="/_nuxt/34083f3.js" as="script" />
<style data-vue-ssr-id="7e56e4e3:0 56b15182:0 1b7833da:0">.nuxt-progress{position:fixed;top:0;left:0;right:0;height:2px;width:0;opacity:1;transition:width .1s,opacity .4s;background-color:#000;z-index:999999}.nuxt-progress.nuxt-progress-notransition{transition:none}.nuxt-progress-failed{background-color:red}html{font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:16px;word-spacing:1px;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;box-sizing:border-box}*,:after,:before{box-sizing:border-box;margin:0}.button--green{display:inline-block;border-radius:4px;border:1px solid #3b8070;color:#3b8070;text-decoration:none;padding:10px 30px}.button--green:hover{color:#fff;background-color:#3b8070}.button--grey{display:inline-block;border-radius:4px;border:1px solid #35495e;color:#35495e;text-decoration:none;padding:10px 30px;margin-left:15px}.button--grey:hover{color:#fff;background-color:#35495e}.container{margin:0 auto;padding:10px}</style>
</head>
<body>
<div data-server-rendered="true" id="__nuxt">
<!---->
<div id="__layout">
<div>
<div class="container">
<fieldset>
<legend>ssr-asyncData数据</legend>
<div>
姓名: 张老师
</div>
<div>
姓名: 丁老师
</div>
</fieldset>
<fieldset>
<legend>ssr-data数据</legend>
<div>
姓名: 张老师1
</div>
<div>
姓名: 丁老师1
</div>
</fieldset>
<fieldset>
<legend>ssr-created</legend>
<div>
姓名: 张老师2
</div>
<div>
姓名: 丁老师2
</div>
</fieldset>
<fieldset>
<legend>csr-mounted数据</legend>
</fieldset>
</div>
</div>
</div>
</div>
<script>window.__NUXT__=function(e){return{layout:"default",data:[{teachers:[{id:202101,name:"张老师"},{id:202102,name:"丁老师"}]}],fetch:{},error:e,serverRendered:!0,routePath:"/",config:{app:{basePath:"/",assetsPath:"/_nuxt/",cdnURL:e}}}}(null)</script>
<script src="/_nuxt/e4e7482.js" defer=""></script>
<script src="/_nuxt/34083f3.js" defer=""></script>
<script src="/_nuxt/2a8f614.js" defer=""></script>
<script src="/_nuxt/699e2f6.js" defer=""></script>
</body>
</html>
可以看到 ssg和ssr一样,也只会在data、created、asyncDate执行,唯一的和ssr的区别是,服务器不在实时参与渲染。即nuxtJs的执行时机是在这些属性和方法中
关于部署
ssg和csr是一样的,比较简单。编译后 直接拖到服务器容器上就行
ssr则较为麻烦,ssr以来后端node进程(npm start) 最好用pm2管理 不会独占进程