SSR全拼是Server-Side Rendering,服务端渲染。
所谓服务端渲染,指的是把vue组件在服务器端渲染为组装好的HTML字符串,而后将它们直接发送到浏览器,最后须要将这些静态标记混合在客户端上彻底可交互的应用程序。html
①知足seo需求,传统的spa数据都是异步加载的,爬虫引擎没法加载,须要利用ssr将数据直出渲染在页面源代码中。
②更宽的内容达到时间(首屏加载更快),当请求页面的时候,服务端渲染完数据以后,把渲染好的页面直接发送给浏览器,并进行渲染。浏览器只须要解析html不须要去解析js。前端
借用下面的一张图,咱们来简单阐述一下vue-ssr的原理。
咱们能够看到,左侧Source部分就是咱们所编写的源代码,全部代码有一个公共入口,就是app.js,紧接着就是服务端的入口
(entry-server.js)和客户端的入口(entry-client.js)。当完成全部源代码的编写以后,咱们经过webpack的构建,打包出两个bundle,分别是server bundle和client bundle;当用户进行页面访问的时候,先是通过服务端的入口,将vue组建组装为html字符串,并混入客户端所访问的html模板中,最终就完成了整个ssr渲染的过程。vue
在终端输入如下命令node
mkdir ssr-demo cd ssr-demo npm init
因为咱们这个只是一个demo项目,能够直接一路按回车键,直接忽略配置。
完成以后咱们能够看到文件夹里面有一个package.json的文件,这就是配置表。webpack
该项目须要四个依赖,依次安装git
npm install express npm install vue npm install vue-router npm install vue-server-renderer
其中express使咱们node端的框架,vue用于建立vue实例,vue-router则用于实现路由控制,最后vue-server-renderer尤其关键,咱们实现的vue-ssr依靠于这个库提供的API。
在安装依赖完毕以后,咱们看到package.json中已经把四个依赖都写上了。github
"express": "^4.17.1", "vue": "^2.6.10", "vue-router": "^3.0.6", "vue-server-renderer": "^2.6.10"
在根目录下咱们新建一个server.js,用户搭建node服务web
const express = require("express"); const app = express(); app.get('*', (request, response) => { response.end('hello, ssr'); }) app.listen(3001, () => { console.log('服务已开启') })
接着为了后续开发的便利,咱们在package.json中添加一个启动命令:vue-router
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "server": "node index.js" },
接着咱们在终端输入 npm run server,而后再浏览器输入localhost:3001,即可以看到页面中的文字被成功渲染。vue-cli
在上一步咱们已经能成功渲染出一个文字,可是ssr并非主要为了渲染文字,而是渲染一个html模板。
那么,接下来,咱们得告知浏览器,咱们须要渲染的是html,而不仅是text,所以咱们须要修改响应头。
同时,引入vue-server-renderer中的createRenderer对象,有一个renderToString的方法,能够将vue实例转成html的形式。(renderToString这个方法接受的第一个参数是vue的实例,第二个参数是一个回调函数,若是不想使用回调函数的话,这个方法也返回了一个Promise对象,当方法执行成功以后,会在then函数里面返回html结构。)
修改server.js以下:
const express = require("express"); const app = express(); const Vue = require("vue"); const vueServerRender = require("vue-server-renderer").createRenderer(); app.get('*', (request, response) => { const vueApp = new Vue({ data:{ message: "hello, ssr" }, template: `<h1>{{message}}</h1>` }); response.status(200); response.setHeader("Content-type", "text/html;charset-utf-8"); vueServerRender.renderToString(vueApp).then((html) => { response.end(html); }).catch(err => console.log(err)) }) app.listen(3001, () => { console.log('服务已开启') })
保存代码,重启服务,而后从新刷新页面。咱们发现,页面好像没什么不一样,就是字体变粗了而已。其实并非,你能够尝试查看页面源代码,咱们发如今源代码中,已经存在一个标签对h1,这就是html模板的雏形。同时,细心的同窗还会发现,h1上面有一个属性:
data-server-rendered="true",那这个属性是干什么的呢?这个是一个标记,代表这个页面是由vue-ssr渲染而来的。你们不妨能够打开一些seo页面或者一些公司的网站,查看源代码,你会发现,也是有这个标记。
虽然h1标签对被成功渲染,可是咱们发现这个html页面并不完整, 他缺乏了文档声明,html标签,body标签,title标签等。
建立一个index.html,用于挂载Vue实例。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Hello, SSR</title> </head> <body> <!--vue-ssr-outlet--> </body> </html>
注意,body中的注释不能去掉,这是Vue挂载的占位符。
而后修改server.js,将html模板引进去。这里咱们在createRenderer函数能够接收一个对象做为配置参数。配置参数中有一项为template,这项配置的就是咱们即将使用的Html模板。这个接收的不是一个单纯的路径,咱们须要使用fs模块将html模板读取出来。
let path = require("path"); const vueServerRender = require("vue-server-renderer").createRenderer({ template:require("fs").readFileSync(path.join(__dirname,"./index.html"),"utf-8") });
保存代码,重启服务,而后从新刷新页面。咱们查看源代码,发现,已经能成功渲染出一个完整的页面了。
上面的开发模式,很显然只是一个demo而已,接下来咱们模拟一下正常的vue开发的目录结构。
建立一个src文件夹,里面有一个router文件夹,再有一个index,js用做路由,并建立一个app.js,用做vue的入口,以下图:
修改router/index.js
const vueRouter = require("vue-router"); const Vue = require("vue"); Vue.use(vueRouter); module.exports = () => { return new vueRouter({ mode:"history", routes:[ { path:"/", component:{ template:`<h1>this is home page</h1>` }, name:"home" }, { path:"/about", component:{ template:`<h1>this is about page</h1>` }, name:"about" } ] }) }
修改app.js
const Vue = require("vue"); const createRouter = require("./router") module.exports = (context) => { const router = createRouter(); return new Vue({ router, data:{ message:"Hello,Vue SSR!", }, template:` <div> <h1>{{message}}</h1> <ul> <li> <router-link to="/">home</router-link> </li> <li> <router-link to="/about">about</router-link> </li> </ul> <router-view></router-view> </div> ` }); }
而后在server.js中,将app.js引入
const express = require("express"); const app = express(); const vueApp = require('./src/app.js'); let path = require("path"); const vueServerRender = require("vue-server-renderer").createRenderer({ template:require("fs").readFileSync(path.join(__dirname,"./index.html"),"utf-8") }); app.get('*', (request, response) => { let vm = vueApp({}); response.status(200); response.setHeader("Content-type", "text/html;charset-utf-8"); vueServerRender.renderToString(vm).then((html) => { response.end(html); }).catch(err => console.log(err)) }) app.listen(3001, () => { console.log('服务已开启') })
保存代码,重启服务,而后从新刷新页面。而后咱们能够看到浏览器的路由已经被成功渲染了,可是不管怎么点击都没反应,浏览器的url有更改,可是页面内容不变。
这是由于咱们只是将页面渲染的工做交给服务端,而页面路由切换,仍是在前端执行,服务端并未能接收到该指令,所以不管怎么切换路由,服务端渲染出来的页面根本没变化。
在src中建立一个entry-server.js文件,该文件为服务端入口文件,接收app和router实例:
const createApp = require("./app.js"); module.exports = (context) => { return new Promise(async (reslove,reject) => { let {url} = context; let {app,router} = createApp(context); router.push(url); // router回调函数 // 当全部异步请求完成以后就会触发 router.onReady(() => { let matchedComponents = router.getMatchedComponents(); if(!matchedComponents.length){ return reject(); } reslove(app); },reject) }) }
在src中建立一个entry-client.js文件,该文件为客户端入口,负责将路由挂载到app里面。
const createApp = require("./app.js"); let {app,router} = createApp({}); router.onReady(() => { app.$mount("#app") });
修改app.js,将router和vue实例暴露出去
const Vue = require("vue"); const createRouter = require("./router") module.exports = (context) => { const router = createRouter(); const app = new Vue({ router, data:{ message:"Hello,Vue SSR!", }, template:` <div> <h1>{{message}}</h1> <ul> <li> <router-link to="/">home</router-link> </li> <li> <router-link to="/about">about</router-link> </li> </ul> <router-view></router-view> </div> ` }); return { app, router } }
最终修改server.js
const express = require("express"); const app = express(); const App = require('./src/entry-server.js'); let path = require("path"); const vueServerRender = require("vue-server-renderer").createRenderer({ template:require("fs").readFileSync(path.join(__dirname,"./index.html"),"utf-8") }); app.get('*', async(request, response) => { response.status(200); response.setHeader("Content-type", "text/html;charset-utf-8"); let {url} = request; let vm; vm = await App({url}) vueServerRender.renderToString(vm).then((html) => { response.end(html); }).catch(err => console.log(err)) }) app.listen(3001, () => { console.log('服务已开启') })
保存代码,重启服务,而后从新刷新页面。这时候,咱们发现页面的路由切换生效了,而且不一样页面的源代码也不同了。
既然是服务端渲染,数据的接收也是来源于服务端,那怎样才能把服务端接收到的数据传输给前端,而后进行渲染呢?
修改entry-server.js,进行同步或者异步获取数据
const createApp = require("./app.js"); const getData = function(){ return new Promise((reslove, reject) => { let str = 'this is a async data!'; reslove(str); }) } module.exports = (context) => { return new Promise(async (reslove,reject) => { let {url} = context; // 数据传递 context.propsData = 'this is a data from props!' context.asyncData = await getData(); let {app,router} = createApp(context); router.push(url); // router回调函数 // 当全部异步请求完成以后就会触发 router.onReady(() => { let matchedComponents = router.getMatchedComponents(); if(!matchedComponents.length){ return reject(); } reslove(app); },reject) }) }
修改app.js,接收数据并渲染
const Vue = require("vue"); const createRouter = require("./router") module.exports = (context) => { const router = createRouter(); const app = new Vue({ router, data:{ message:"Hello,Vue SSR!", propsData: context.propsData, asyncData: context.asyncData }, template:` <div> <h1>{{message}}</h1> <p>{{asyncData}}</p> <p>{{propsData}}</p> <ul> <li> <router-link to="/">home</router-link> </li> <li> <router-link to="/about">about</router-link> </li> </ul> <router-view></router-view> </div> ` }); return { app, router } }
最后咱们能够看到不管是同步仍是异步获取的数据,都能成功地经过服务端渲染,展现在页面源代码中。
另外,你也能够在server.js中的request中,将数据传递下去。
实现了一个简易版本的vue-ssr,下期咱们会依赖于vue-cli,进行webpack改造,实现一个通用且更实用的vue-ssr框架。从零开始搭建一个vue-ssr(下)