Vue服务端渲染和Vue浏览器端渲染的性能对比(实例PK )


Posted in Javascript onMarch 31, 2017

Vue 2.0 开始支持服务端渲染的功能,所以本文章也是基于vue 2.0以上版本。网上对于服务端渲染的资料还是比较少,最经典的莫过于Vue作者尤雨溪大神的 vue-hacker-news。本人在公司做Vue项目的时候,一直苦于产品、客户对首屏加载要求,SEO的诉求,也想过很多解决方案,本次也是针对浏览器渲染不足之处,采用了服务端渲染,并且做了两个一样的Demo作为比较,更能直观的对比Vue前后端的渲染。

talk is cheap,show us the code!话不多说,我们分别来看两个Demo:(欢迎star 欢迎pull request)

1.浏览器端渲染Demo: https://github.com/monkeyWangs/doubanMovie

2.服务端渲染Demo:https://github.com/monkeyWangs/doubanMovie-SSR

两套代码运行结果都是为了展示豆瓣电影的,运行效果也都是差不多,下面我们来分别简单的阐述一下项目的机理:

一、浏览器端渲染豆瓣电影

首先我们用官网的脚手架搭建起来一个vue项目

npm install -g vue-cli
vue init webpack doubanMovie
cd doubanMovie
npm install
npm run dev

这样便可以简单地打起来一个cli框架,下面我们要做的事情就是分别配置 vue-router, vuex,然后配置我们的webpack proxyTable 让他支持代理访问豆瓣API。

1.配置Vue-router

我们需要三个导航页:正在上映、即将上映、Top250;一个详情页,一个搜索页。这里我给他们分别配置了各自的路由。在 router/index.js 下配置以下信息:

import Vue from 'vue'
import Router from 'vue-router'
import Moving from '@/components/moving'
import Upcoming from '@/components/upcoming'
import Top250 from '@/components/top250'
import MoviesDetail from '@/components/common/moviesDetail'
import Search from '@/components/searchList'
Vue.use(Router)
/**
 * 路由信息配置
 */
export default new Router({
 routes: [
 {
 path: '/',
 name: 'Moving',
 component: Moving
 },
 {
 path: '/upcoming',
 name: 'upcoming',
 component: Upcoming
 },
 {
 path: '/top250',
 name: 'Top250',
 component: Top250
 },
 {
 path: '/search',
 name: 'Search',
 component: Search
 },
 {
 path: '/moviesDetail',
 name: 'moviesDetail',
 component: MoviesDetail
 }
 ]
})

这样我们的路由信息配置好了,然后每次切换路由的时候,尽量避免不要重复请求数据,所以我们还需要配置一下组件的keep-alive:在app.vue组件里面。

<keep-alive exclude="moviesDetail">
 <router-view></router-view>
</keep-alive>

这样一个基本的vue-router就配置好了。

2.引入vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

简而言之:Vuex 相当于某种意义上设置了读写权限的全局变量,将数据保存保存到该“全局变量”下,并通过一定的方法去读写数据。

Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:

应用层级的状态应该集中到单个 store 对象中。

提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。

异步逻辑都应该封装到 action 里面。

对于大型应用我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:

├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
 ├── index.js  # 我们组装模块并导出 store 的地方
 └── moving  # 电影模块
 ├── index.js # 模块内组装,并导出模块的地方
 ├── actions.js # 模块基本 action
 ├── getters.js # 模块级别 getters
 ├── mutations.js # 模块级别 mutations
 └── types.js # 模块级别 types

所以我们开始在我们的src目录下新建一个名为store 的文件夹 为了后期考虑 我们新建了moving 文件夹,用来组织电影,考虑到所有的action,getters,mutations,都写在一起,文件太混乱,所以我又给他们分别提取出来。

stroe文件夹建好,我们要开始在main.js里面引用vuex实例:

import store from './store'
new Vue({
 el: '#app',
 router,
 store,
 template: '<App/>',
 components: { App }
})

这样,我们便可以在所有的子组件里通过 this.$store 来使用vuex了。

3.webpack proxyTable 代理跨域

webpack 开发环境可以使用proxyTable 来代理跨域,生产环境的话可以根据各自的服务器进行配置代理跨域就行了。在我们的项目config/index.js 文件下可以看到有一个proxyTable的属性,我们对其简单的改写

proxyTable: {
 '/api': {
 target: 'http://api.douban.com/v2',
 changeOrigin: true,
 pathRewrite: {
  '^/api': ''
 }
 }
 }

这样当我们访问

localhost:8080/api/movie

的时候 其实我们访问的是

http://api.douban.com/v2/movie

这样便达到了一种跨域请求的方案。

至此,浏览器端的主要配置已经介绍完了,下面我们来看看运行的结果:

Vue服务端渲染和Vue浏览器端渲染的性能对比(实例PK )

为了介绍浏览器渲染是怎么回事,我们运行一下npm run build 看看我们的发布版本的文件,到底是什么鬼东西....

run build 后会都出一个dist目录 ,我们可以看到里面有个index.html,这个便是我们最终页面将要展示的html,我们打开,可以看到下面:

Vue服务端渲染和Vue浏览器端渲染的性能对比(实例PK )

观察好的小伙伴可以发现,我们并没有多余的dom元素,就只有一个div,那么页面要怎么呈现呢?答案是js append,对,下面的那些js会负责innerHTML。而js是由浏览器解释执行的,所以呢,我们称之为浏览器渲染,这有几个致命的缺点:

js放在dom结尾,如果js文件过大,那么必然造成页面阻塞。用户体验明显不好(这也是我我在公司反复被产品逼问的事情)

不利于SEO

客户端运行在老的JavaScript引擎上

对于世界上的一些地区人,可能只能用1998年产的电脑访问互联网的方式使用计算机。而Vue只能运行在IE9以上的浏览器,你可能也想为那些老式浏览器提供基础内容 - 或者是在命令行中使用 Lynx的时髦的黑客

基于以上的一些问题,服务端渲染呼之欲出....

二、服务器端渲染豆瓣电影

先看一张Vue官网的服务端渲染示意图

Vue服务端渲染和Vue浏览器端渲染的性能对比(实例PK )

从图上可以看出,ssr 有两个入口文件,client.js 和 server.js, 都包含了应用代码,webpack 通过两个入口文件分别打包成给服务端用的 server bundle 和给客户端用的 client bundle. 当服务器接收到了来自客户端的请求之后,会创建一个渲染器 bundleRenderer,这个 bundleRenderer 会读取上面生成的 server bundle 文件,并且执行它的代码, 然后发送一个生成好的 html 到浏览器,等到客户端加载了 client bundle 之后,会和服务端生成的DOM 进行 Hydration(判断这个DOM 和自己即将生成的DOM 是否相同,如果相同就将客户端的vue实例挂载到这个DOM上, 否则会提示警告)。

具体实现:

我们需要vuex,需要router,需要服务器,需要服务缓存,需要代理跨域....不急我们慢慢来。

1.建立nodejs服务

首先我们需要一个服务器,那么对于nodejs,express是很好地选择。我们来建立一个server.js

const port = process.env.PORT || 8080
app.listen(port, () => {
 console.log(`server started at localhost:${port}`)
})

这里用来启动服务监听 8080 端口。

然后我们开始处理所有的get请求,当请求页面的时候,我们需要渲染页面

app.get('*', (req, res) => {
 if (!renderer) {
 return res.end('waiting for compilation... refresh in a moment.')
 }
 const s = Date.now()
 res.setHeader("Content-Type", "text/html")
 res.setHeader("Server", serverInfo)
 const errorHandler = err => {
 if (err && err.code === 404) {
 res.status(404).end('404 | Page Not Found')
 } else {
 // Render Error Page or Redirect
 res.status(500).end('500 | Internal Server Error')
 console.error(`error during render : ${req.url}`)
 console.error(err)
 }
 }
 renderer.renderToStream({ url: req.url })
 .on('error', errorHandler)
 .on('end', () => console.log(`whole request: ${Date.now() - s}ms`))
 .pipe(res)
})

然后我们需要代理请求,这样才能进行跨域,我们引入http-proxy-middleware模块:

const proxy = require('http-proxy-middleware');//引入代理中间件
/**
 * proxy middleware options
 * 代理跨域配置
 * @type {{target: string, changeOrigin: boolean, pathRewrite: {^/api: string}}}
 */
var options = {
 target: 'http://api.douban.com/v2', // target host
 changeOrigin: true,  // needed for virtual hosted sites
 pathRewrite: {
 '^/api': ''
 }
};
var exampleProxy = proxy(options);
app.use('/api', exampleProxy);

这样我们的服务端server.js便配置完成。接下来 我们需要配置服务端入口文件,还有客户端入口文件,首先来配置一下客户端文件,新建src/entry-client.js

import 'es6-promise/auto'
import { app, store, router } from './app'
// prime the store with server-initialized state.
// the state is determined during SSR and inlined in the page markup.
if (window.__INITIAL_STATE__) {
 store.replaceState(window.__INITIAL_STATE__)
}
/**
 * 异步组件
 */
router.onReady(() => {
 // 开始挂载到dom上
 app.$mount('#app')
})
// service worker
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
 navigator.serviceWorker.register('/service-worker.js')
}

客户端入口文件很简单,同步服务端发送过来的数据,然后把 vue 实例挂载到服务端渲染的 DOM 上。

再配置一下服务端入口文件:src/entry-server.js

import { app, router, store } from './app'
const isDev = process.env.NODE_ENV !== 'production'
// This exported function will be called by `bundleRenderer`.
// This is where we perform data-prefetching to determine the
// state of our application before actually rendering it.
// Since data fetching is async, this function is expected to
// return a Promise that resolves to the app instance.
export default context => {
 const s = isDev && Date.now()
 return new Promise((resolve, reject) => {
 // set router's location
 router.push(context.url)
 // wait until router has resolved possible async hooks
 router.onReady(() => {
 const matchedComponents = router.getMatchedComponents()
 // no matched routes
 if (!matchedComponents.length) {
 reject({ code: 404 })
 }
 // Call preFetch hooks on components matched by the route.
 // A preFetch hook dispatches a store action and returns a Promise,
 // which is resolved when the action is complete and store state has been
 // updated.
 Promise.all(matchedComponents.map(component => {
 return component.preFetch && component.preFetch(store)
 })).then(() => {
 isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`)
 // After all preFetch hooks are resolved, our store is now
 // filled with the state needed to render the app.
 // Expose the state on the render context, and let the request handler
 // inline the state in the HTML response. This allows the client-side
 // store to pick-up the server-side state without having to duplicate
 // the initial data fetching on the client.
 context.state = store.state
 resolve(app)
 }).catch(reject)
 })
 })
}

server.js 返回一个函数,该函数接受一个从服务端传递过来的 context 的参数,将 vue 实例通过 promise 返回。context 一般包含 当前页面的url,首先我们调用 vue-router 的 router.push(url) 切换到到对应的路由, 然后调用 getMatchedComponents 方法返回对应要渲染的组件, 这里会检查组件是否有 fetchServerData 方法,如果有就会执行它。

下面这行代码将服务端获取到的数据挂载到 context 对象上,后面会把这些数据直接发送到浏览器端与客户端的vue 实例进行数据(状态)同步。

context.state = store.state

然后我们分别配置客户端和服务端webpack,这里可以在我的github上fork下来参考配置,里面每一步都有注释,这里不再赘述。

接着我们需要创建app.js:

import Vue from 'vue'
import App from './App.vue'
import store from './store'
import router from './router'
import { sync } from 'vuex-router-sync'
import Element from 'element-ui'
Vue.use(Element)
// sync the router with the vuex store.
// this registers `store.state.route`
sync(store, router)
/**
 * 创建vue实例
 * 在这里注入 router store 到所有的子组件
 * 这样就可以在任何地方使用 `this.$router` and `this.$store`
 * @type {Vue$2}
 */
const app = new Vue({
 router,
 store,
 render: h => h(App)
})
/**
 * 导出 router and store.
 * 在这里不需要挂载到app上。这里和浏览器渲染不一样
 */
export { app, router, store }

这样 服务端入口文件和客户端入口文件便有了一个公共实例Vue, 和我们以前写的vue实例差别不大,但是我们不会在这里将app mount到DOM上,因为这个实例也会在服务端去运行,这里直接将 app 暴露出去。

接下来创建路由router,创建vuex跟客户端都差不多。详细的可以参考我的项目...

到此,服务端渲染配置 就简单介绍完了,下面我们启动项目简单的看下:

Vue服务端渲染和Vue浏览器端渲染的性能对比(实例PK )

这里跟服务端界面一样,不一样的是url已经不是之前的 #/而变成了请求形式 /

这样每当浏览器发送一个页面的请求,会有服务器渲染出一个dom字符串返回,直接在浏览器段显示,这样就避免了浏览器端渲染的很多问题。

说起SSR,其实早在SPA (Single Page Application) 出现之前,网页就是在服务端渲染的。服务器接收到客户端请求后,将数据和模板拼接成完整的页面响应到客户端。 客户端直接渲染, 此时用户希望浏览新的页面,就必须重复这个过程, 刷新页面. 这种体验在Web技术发展的当下是几乎不能被接受的,于是越来越多的技术方案涌现,力求 实现无页面刷新或者局部刷新来达到优秀的交互体验。但是SEO却是致命的,所以一切看应用场景,这里只为大家提供技术思路,为vue开发提供多一种可能的方案。

为了更清晰的对比两次渲染的结果,我做了一次实验,把两个想的项目build后模拟生产环境,在浏览器netWork模拟网速3g环境,先来看看服务端渲染的结果:

Vue服务端渲染和Vue浏览器端渲染的性能对比(实例PK )

可以看到整体加载dom一共花了832ms;用户可能在网络比较慢的情况下从远处访问网站 - 或者通过比较差的带宽。 这些情况下,尽量减少页面请求数量,来保证用户尽快看到基本的内容。

Vue服务端渲染和Vue浏览器端渲染的性能对比(实例PK )

然后我们可以看到其中有一个vendor.js 达到了563KB,整体的加载时间达到了了8.19s,这是因为单页面文件的原因,会把所有的逻辑代码打包到一个js里面。可以用分webpack拆分代码避免强制用户下载整个单页面应用,但是,这样也远没有下载个单独的预先渲染过的HTML文件性能高。

以上所述是小编给大家介绍的Vue服务端渲染和Vue浏览器端渲染的性能对比(实例PK ),希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
Jquery之Ajax运用 学习运用篇
Sep 26 Javascript
AngularJS Module方法详解
Dec 08 Javascript
简单三步实现报表页面集成天气
Dec 15 Javascript
Node.js获取前端ajax提交的request信息
Feb 20 Javascript
javascript实现的图片预览功能
Mar 25 Javascript
利用webstrom调试Vue.js单页面程序的方法教程
Jun 06 Javascript
javascript 中select框触发事件过程的分析
Aug 01 Javascript
React-native桥接Android原生开发详解
Jan 17 Javascript
mpvue性能优化实战技巧(小结)
Apr 17 Javascript
微信小程序开发实现的选项卡(窗口顶部/底部TabBar)页面切换功能图文详解
May 14 Javascript
只有 20 行的 JavaScript 模板引擎实例详解
May 11 Javascript
JS中的变量作用域(console版)
Jul 18 Javascript
ES6新特性之Object的变化分析
Mar 31 #Javascript
ES6新数据结构Set与WeakSet用法分析
Mar 31 #Javascript
ES6新数据结构Map功能与用法示例
Mar 31 #Javascript
基于AGS JS开发自定义贴图图层
Mar 31 #Javascript
Node.js使用Express创建Web项目详细教程
Mar 31 #Javascript
ES6使用let命令更简单的实现块级作用域实例分析
Mar 31 #Javascript
JS解决移动web开发手机输入框弹出的问题
Mar 31 #Javascript
You might like
收音机史话 - 1960年代前后的DIY
2021/03/02 无线电
PHP最常用的ini函数分析 针对PHP.ini配置文件
2010/04/22 PHP
php INI配置文件的解析实现分析
2011/01/04 PHP
使用正则去除php代码中的注释方法
2016/11/03 PHP
PHP操作Redis常用命令的实例详解
2020/12/23 PHP
javascript中使用css需要注意的地方小结
2010/09/01 Javascript
JavaScript Title、alt提示(Tips)实现源码解读
2010/12/12 Javascript
Javascript 面向对象编程(coolshell)
2012/03/18 Javascript
跟我学Nodejs(一)--- Node.js简介及安装开发环境
2014/05/20 NodeJs
JavaScript动态改变HTML页面元素例如添加或删除
2014/08/10 Javascript
javascript中typeof操作符和constucor属性检测
2015/02/26 Javascript
基于jquery实现一个滚动的分步注册向导-附源码
2015/08/26 Javascript
php基于redis处理session的方法
2016/03/14 Javascript
JS上传组件FileUpload自定义模板的使用方法
2016/05/10 Javascript
javascript 正则表达式分组、断言详解
2017/04/20 Javascript
react-native-fs实现文件下载、文本存储的示例代码
2017/09/22 Javascript
js根据后缀判断文件文件类型的代码
2020/05/09 Javascript
JavaScript中的执行环境和作用域链
2020/09/04 Javascript
移动端JS实现拖拽两种方法解析
2020/10/12 Javascript
python通过shutil实现快速文件复制的方法
2015/03/14 Python
Python进阶篇之字典操作总结
2016/11/16 Python
python用BeautifulSoup库简单爬虫实例分析
2018/07/30 Python
python实现简单flappy bird
2018/12/24 Python
Python爬虫实现“盗取”微信好友信息的方法分析
2019/09/16 Python
浅析python,PyCharm,Anaconda三者之间的关系
2019/11/27 Python
Python基于read(size)方法读取超大文件
2020/03/12 Python
html5 兼容IE6结构的实现代码
2012/05/14 HTML / CSS
Auchan Direct波兰:欧尚在线杂货店
2016/10/19 全球购物
REISS美国官网:伦敦最受欢迎的时尚品牌
2019/08/16 全球购物
毕业生自我鉴定
2013/12/04 职场文书
环保标语口号
2014/06/13 职场文书
党员教师群众路线对照检查材料思想汇报
2014/09/29 职场文书
小学生手册家长意见
2015/06/03 职场文书
Python通过m3u8文件下载合并ts视频的操作
2021/04/16 Python
Python函数式编程中itertools模块详解
2021/09/15 Python
深入理解mysql事务隔离级别和存储引擎
2022/04/12 MySQL