读本文前,请先熟读
nuxt
官方文档,并且具备一定的vue.js
相关开发经验
中文文档
英文文档
vue SSR 指南
一、CSR 和 SSR 对比
在 SPA
之前的时代,我们传统的 Web
架构大都是 SSR
,如:WordPress(PHP)
、JSP
技术、JavaWeb
等这些程序都是传统典型的 SSR
架构,即:服务端取出数据和模板组合生成 html
输出给前端,前端发生请求时,重新向服务端请求 html
资源。
SPA(CSR):
SPA
应用,到了 Vue
、React
,单页面应用优秀的用户体验,逐渐成为了主流,页面整体是javaScript
渲染出来的,称之为客户端渲染 CSR
。SPA
渲染过程。由客户端访问 URL
发送请求到服务端,返回 HTML
结构(但是 SPA
的返回的 HTML
结构是非常的小的,只有一个基本的结构)。客户端接收到返回结果之后,在客户端开始渲染 HTML
,渲染时执行对应javaScript
,最后渲染template
,渲染完成之后,再次向服务端发送数据请求,注意这里时数据请求,服务端返回json
格式数据。客户端接收数据,然后完成最终渲染。
CSR 原理图
CSR 多数是基于 webpack 构建的项目,编译出来的 html 文件,资源文件都被打包到 js 中,这样的页面是不利于搜索 引擎优化 (SEO, Search Engine Optimization)
,并且 内容到达时间 (time-to-content)(或称之为首屏渲染时长)
也有很大的优化空间
简单来讲,SPA
虽然给服务器减轻了压力,也存在比较明显的两个缺点:
- 首屏渲染时间比较长:必须等待
JavaScript
加载完毕,并且执行完毕,才能渲染出首屏。 -
SEO
不友好:爬虫只能拿到一个div
元素,认为页面是空的,不利于SEO
。
什么是 SEO
呢?SEO
即通过各种技术(手段)来确保,你的 Web
内容被搜素引擎最大化收录,最大化提高权重,带来更多流量。大部分的搜索引擎仅能抓取 URI
直接输出的数据资源,对于 Ajax
类的异步请求的数据无法抓取
因此,对于那些展示宣传型页面,如官网,必须进行服务端渲染
SSR:
为了解决如上两个问题,出现了 SSR
解决方案,后端渲染出首屏的 DOM
结构返回,前端拿到内容带上首屏,后续的页面操作,再用单页面路由和渲染,称之为服务端渲染(SSR)
。
SSR
渲染流程是这样的,客户端发送 URL
请求到服务端,在服务端做出 html
和数据
的渲染,渲染完成之后返回 html
结构,客户端拿到页面的 html
结构渲染首屏。所以用户在浏览首屏的时候速度会很快,因为客户端不需要再次发送 ajax
请求。并不是做了 SSR
我们的页面就不属于 SPA
应用了,它仍然是一个独立的 spa
应用。
SSR 原理图
SSR
是处于 CSR
与SPA
应用之间的一个折中的方案,在渲染首屏的时候在服务端做出了渲染,注意仅仅是首屏,其他页面还是需要在客户端渲染的,在 服务端
接收到请求之后并且渲染出首屏页面,会携带着剩余的路由信息预留给 客户端
去渲染其他路由的页面。
vueSSR
将本来要放在浏览器执行创建的组件,放到服务端先创建好,然后生成对应的 html 将它们直接发送到浏览器,最后将这些静态标记 ” 激活 ” 为客户端上完全可交互的应用程序。
在浏览器第一次访问某个 URI
资源的时候(首屏),Web
服务器根据路由拿到对应数据渲染并输出,且输出的数据中包含两部分:
- 路由页对应的页面及已渲染好的数据
- 完整的 SPA 程序代码
在首屏渲染完成之后,此时我们看到的其实已经是一个和之前的 SPA
相差无几的应用程序了,接下来我们进行的任何操作都只是客户端的应用进行交互,页面 / 组件由 Web
端渲染,路由也由浏览器控制,用户只需要和当前浏览器内的应用打交道就可以了。
vueSSR 原理图
webpack
将 Source
打包出两个 bundle
文件:其中 Server Bundle
用于服务端渲染,服务端通过渲染器 bundleRenderer
将 bundle
生成首屏 html
片段;而 Client Bundle
用于客户端渲染,首屏外的交互和数据处理还是需要浏览器执行 Client Bundle
来完成
缺点:
- 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数
(lifecycle hook)
中使用;一些外部扩展库(external library)
可能需要特殊处理,才能在服务器渲染应用程序中运行。 - 更多的服务器端负载。在
Node.js
中渲染完整的应用程序,显然会比仅仅提供静态文件的server
更加大量占用CPU
资源,因此如果你预料在高流量环境(high traffic)
下使用,请准备相应的服务器负载,并明智地采用缓存策略。
二、nuxt.js 介绍
1. nuxt.js 是什么?
Nuxt.js
是vue
官方推荐的一个基于Vue.js
的做Vue SSR
的通用应用框架(开箱即用),集成了Vue,Vue-Router,Vuex,Vue-Meta
等组件 / 框架,内置了webpack
用于自动化构建,使我们可以更快速地搭建一个具有服务端渲染能力的应用。
2. nuxt.js 的优势?
作为框架,Nuxt.js
为 客户端 / 服务端 这种典型的应用架构模式提供了许多有用的特性,例如异步数据加载、中间件支持、布局支持等。Nuxt.js
有以下比较明显的特性
- 支持各种样式预编译器
SASS,LESS
等等 - 本地开发支持热加载
-
HTML
头部标签管理(依赖vue-meta
实现) - 自动代码分层
- 强大的路由功能,支持异步数据(路由无需额外配置)
- 内置
webpack
配置,无需额外配置
3. nuxt.js 的使用
npm create nuxt-app <project-name>
4. nuxt.js 目录结构
(layouts、pages、static、store、nuxt.config.js、package.json)是 Nuxt 保留的,不可以更改
5. nuxt.js 渲染流程
- Incoming Request指的是浏览器发出一个请求,服务端接收请求后
- 要检查当前有没有 nuxtServerInit 这个配置项,如果有的话就先执行这个函数。具体的作用和使用可参考官方文档 nuxtserverinit- 方法
-
middleware中间件,中间件允许您定义一个自定义函数运行在一个页面或一组页面渲染之前。也就是可以在 匹配布局(
layout
组件)前执行某种操作,也可以在解析完layout
之后,解析page
组件前 执行某种操作。可以理解为是路由器的拦截器的作用 - 验证:validate(),可以配合高级动态路由去做验证。如果校验不通过,
Nuxt.js
将自动加载显示404
错误页面或500
错误页面,或者进行重定向。 - 获取数据,asyncData方法获取数据并返回给当前组件,fetch方法修改
vuex
的store
-
render
:最后进行渲染。将渲染后的页面返回给浏览器,用户在页面进行操作,如果再次请求新的页面,此时只会回到生命周期中的 middlerware 中,而非 nuxtServerInit。 -
nuxt-link
,如果是发起一个新的路由,那么这个时候要从头开始循环
我们把服务器端创建的 .vue
文件全部理解成组件,在服务器端环境 (node)
通过 beforeCreate
和 created
这俩个生命周期节点后服务器端 vue
组件生命周期结束。返回页面给浏览器,在客户端环境 (v8)
中这个 vue
组件实例创建后会在客户端再次拥有生命周期,此时生命周期中有 mounted
等钩子函数。
需要特别注意的是 nuxt
中没有 mounted
钩子函数也没有组件实例,只有 beforeCreate/created
钩子与 context
对象。beforeCreated()
和 created()
这两个生命周期函数是同时运行在服务端 && 客户端,vue 的其他钩子则运行在客户端,所以 beforeCreated()
和created()
不存在 window
对象
三、nuxt.js 渲染过程部分详解
1、nuxtServerInit
举例:打开网页要立即显示的内容
SSR 方式:
// nuxtServerInit 方法
actions: {async nuxtServerInit({commit},{req,app}) {let {data: {province, city}} = await axios.get('/aa/bb')
commit('home/setPosition',{province: '', city:''})
}
}
// middleware 属性
middleware: async (ctx) => {let {data: {province, city}} = await axios.get('/aa/bb')
}
NO-SSR
vue 组件 mounted 函数发送请求
2、异步数据 asyncData
asyncData
方法会在组件(限于页面组件)每次加载渲染之前,即在服务端或路由更新之前被调用。在asyncData()
中可以处理请求得来的数据,通过return
将处理后的数据返回给当前vue
组件的data
。再次强调这里不能使用this
,因为没有组件实例,asyncData()
默认的参数是ctx
即content
对象。
该方法用来获取数据,在服务器端把异步获取到的数据扔给浏览器,那是如何抛给浏览器的呢?
通过下发一个 `script` 标签,然后在 `window` 上挂了一个对象
这个对象,第一个是告诉你用的是哪个模板,第二个给你的是数据
3、布局
Nuxt.js 布局方式如下图所示:
nuxt.js
实现了一个新的概念,layout
布局,我们可以通过 layout
布 局方便的实现页面的多个布局之间方便的切换。具体开发的页面中,如果使用默认布局,则不需指定页面的布局,nuxt
框架会自动对没有指定布局的页面和 default
布局进行关联。如果需要指定布局,则在 layout
字段中对布局进行指定。
<script>
export default {
layout: 'plusBuy',
...
}
</script>
// 如果 layout 文件中建立了一个单独的文件,则在使用中也要指定
<script>
export default {
layout: 'plusBuy/plusBuy',
...
}
</script>
四、nuxt 爬坑
1、localhost
访问可以,换成真实的 ip
地址后访问不了
解决方案:
- 确认有没有开代理
- 在
package.json
里做如下配置
"config": {
"nuxt": {
"host": "0.0.0.0",
"port": 3000
}
}
2、接口跨域问题
解决方案
- 安装
@nuxtjs/axios
、@nuxtjs/proxy
- 在
nuxt.config.js
做如下配置
modules: ['@nuxtjs/axios'], // 不需要加入 @nuxtjs/proxy
axios: {proxy: true},
proxy: {
'/wlfrontend': { // 请求到 /wlfrontend 代理到请求 http://10.102.140.38:7001/wlfrontend
target: 'http://10.102.140.38:7001',
changeOrigin: true // 如果接口跨域,需要进行这个参数配置
},
'/scenery': { // 将 'localhost:8080/scenery/xxx' 代理到 'https://m.ly.com/scenery/xxx'
target: 'https://m.ly.com', // 代理地址
changeOrigin: true, // 如果接口跨域,需要进行这个参数配置
secure: false // 默认情况下,不接受运行在 HTTPS 上,且使用了无效证书的后端服务器。如果你想要接受,只要设置 secure: false
}
}
3、asyncDate fetch created
因为服务端客户端都会走,如果不想在客户端执行?
async asyncData ({query, store, req}) {if (!process.server) return
}
async fetch({store, params}){if (!process.server) return
}
created(){if (!process.server) return
},
4、页面做缓存,也就是返回上一级保持数据不重新请求
解决方案:
在布局页面处理,layout/default.vue
或者是自己建立的布局页面
<template>
<div class="plusBuy">
<nuxt keep-alive />
</div>
</template>
5、nuxt
是把所有页面的 js 都引入到主页了?
在生产模式下,
Nuxt.js
使用浏览器的预加载策略来预加载目标页面的脚本资源。所以当用户点击某个链接时,会有一种秒开的感觉。预加载策略使得Nuxt.js
既可以保持代码分离又能保证页面访问体验。<nuxt-link>
则是帮我们扩展了自动预获取代码分割页面。可以使用no-prefetch
属性 禁用
如果想要禁用,在nuxt.config.js
做如下配置
router: {prefetchLinks: false, // 全局禁用所有链接上的预取}
render: {resourceHints: false, // 添加 prefetch 和 preload,以加快初始化页面加载时间。如果有许多页面和路由,可禁用此项},
6、切换子路由的 head
中外部引入脚本载入有延迟,所以在调用时报错
注意:
1、引入脚本不要加 async:true
,这样的话脚本不会阻塞,在下面代码有用到该脚本中的方式时,脚本可能还没有加载完
2、需要每个小项目自己做个定制化页面layout
,layout/ 我的目录 / 我的页面.vue 然后在定制化页面中使用head()
加入脚本
export default {
// 方式一:head: {
script: [{ type: 'text/javascript', src: 'https://js.40017.cn/cn/min/??/touch/hb/c/bridge.2.1.4.js?v=2016053', defer: true}
]
}
// 方式二:head () {
return {
script: [{ type: 'text/javascript', src: 'https://js.40017.cn/cn/min/??/touch/hb/c/bridge.2.1.4.js?v=2016053', defer: true}
]
}
}
}
7、滚动事件
如果 html
和body
设置了 100%,那么子页面足够长时滚动的话,滚动事件要绑定在子页面上,因为 body
的高度不是整个页面的高度
// 1. 在子页面父元素加
<template>
<div class="plus" ref="mainPage"></div>
</template>
// 2. 样式设置 100% 滚动
.plus {
height: 100%;
overflow-y: scroll;
-webkit-overflow-scrolling : touch;
}
// 3. 再添加滚动事件
function scrollEvent() {
var that = this;
let dom = this.$refs.mainPage;
dom.onscroll = function() {
let wh = dom.scrollTop;
// 页面上滑,出现
wh > 100 ? (that.showBackTop = true) : (that.showBackTop = false);
// 未开通,页面滑动至不出现顶部的立即开通按钮时,底部的立即开通固定展示
if(that.memberRightsInfo && !that.memberRightsInfo.IsPlusMember){if(document.querySelector('.tab') && document.querySelector('.tab').offsetTop){let distance = document.querySelector('.tab').offsetTop;
wh > distance - 50 ? (that.isShowFixedBtn = true) : (that.isShowFixedBtn = false);
}
}
};
}
8、文件下建立了其他文件,比如 store/plusBuy/index.js
,并没有在store
下直接建立index.js
,如何使用?
原理:Nuxt 把 store 中的 index.js 文件中所有的 state、mutations、actions、getters 都作为其公共属性挂载到了 store 实例上,然而其他的文件则是使用的是命名空间,其对应的命名空间的名字就是其文件名。
computed: {
...mapState('plusBuy', {nickName: state => state.nickName})
}
...mapMutations('plusBuy', {setCityId: 'setCityId' // 将 `this.setCityId()` 映射为 `this.$store.commit('setCityId')`
})
...mapActions('plusBuy', {login: 'login' // 将 `this.login()` 映射为 `this.$store.dispatch('login')`
})
9、asyncData
不可以调用this
,如果有好多个异步或数据进行处理,如何优化asyncData()
// 可以使用类
class A {aatest(aa){console.log(aa)
}
}
// 调用方法
async asyncData ({query, store, req}) {var test = new A();
test.aatest(123);
}
10、如何获取cookie
// 服务端获取 cookie
b_getToken(req = {},c_name){if (req.headers && req.headers.cookie) {var req_Cookies = req.headers.cookie.split(";")
let tokens = ''
req_Cookies.forEach(v => {if (v.indexOf(c_name + "=")>=0) {tokens = v}
})
return tokens.split('=')[1]
} else {return ''}
}
// 客户端获取 cookie
getCookie: function(c_name) {if (document.cookie.length > 0) {// 先查询 cookie 是否为空,为空就 return ""let c_start = document.cookie.indexOf(c_name +"=") ||''; // 通过 String 对象的 indexOf()来检查这个 cookie 是否存在,不存在就为 -1
if (c_start != -1) {
c_start = c_start + c_name.length + 1; // 最后这个 + 1 其实就是表示 "=" 号啦,这样就获取到了 cookie 值的开始位置
let c_end = document.cookie.indexOf(";", c_start); // 为了得到值的结束位置。因为需要考虑是否是最后一项,所以通过 ";" 号是否存在来判断
if (c_end == -1) {c_end = document.cookie.length;}
return unescape(document.cookie.substring(c_start, c_end));
}
}
return "";
},
// 调用
let token = '';
if(process.server){token = serverUtilsFn.b_getToken(req,'17uCNRefId');
console.log('server:' + token)
}else {token = utilsFn.getCookie('17uCNRefId');
console.log('client:' + token)
}
11、axios
数据处理问题,重复问题
import axios from 'axios';
import requestCheck from './requestCheck';
// 确保使用 axios.create 创建实例后再使用。否则多次刷新页面请求服务器,服务端渲染会重复添加拦截器,导致数据处理错误
const myaxios = axios.create()
// axios.defaults.baseURL = "http://localhost:3000/"
myaxios.interceptors.request.use(config => {let req = {...config};
req.url = req.method.toLocaleLowerCase() == 'post' ? requestCheck(req.url, req.data) : requestCheck(req.url, req.params);
return req;
}, error => {return Promise.reject(error)
})
myaxios.interceptors.response.use(response => {return response}, error => {return Promise.reject(error)
})
export default myaxios;
12、跳转路由传递参数并且取值
传递参数 -- this.$router.push({name: '路由的 name', params: {key: value}})
参数取值 -- this.$route.params.key
注: 使用这种方式,参数不会拼接在路由后面,地址栏上看不到参数
注意: 由于动态路由也是传递 params 的,所以在 this.$router.push() 方法中 path 不能和 params 一起使用,否则 params 将无效。需要用 name 来指定页面。
13、设置页面动画效果
/* 全局过渡动效设置 - 淡出 (fade) 效果 */
.page-enter-active,
.page-leave-active {transition: opacity .5s;}
.page-enter,
.page-leave-active {opacity: 0;}
/* 局部过渡动效设置 - 淡出 (fade) 效果 */
.test-enter-active,
.test-leave-active {transition: opacity .5s;}
.test-enter,
.test-leave-active {opacity: 0;}
// 在要使用的组件页面中
export default {transition: 'test',}
14、如何使用插件
// 1. 安装插件
yarn add swiper -D
// 2. 引入
<script>
import Swiper from 'swiper'
</script>
// 3. 引入样式
<style lang="less" scoped>
@import "../../node_modules/swiper/css/swiper.css";
</style>