从零开始搭建一个 vue-ssr
背景
What?SSR 是什么?
SSR 全拼是 Server-Side Rendering,服务端渲染。
所谓服务端渲染,指的是把 vue 组件在服务器端渲染为组装好的 HTML 字符串,然后将它们直接发送到浏览器,最后需要将这些静态标记混合在客户端上完全可交互的应用程序。
Why? 为什么选择 SSR?
①满足 seo 需求,传统的 spa 数据都是异步加载的,爬虫引擎无法加载,需要利用 ssr 将数据直出渲染在页面源代码中。
②更宽的内容达到时间(首屏加载更快),当请求页面的时候,服务端渲染完数据之后,把渲染好的页面直接发送给浏览器,并进行渲染。浏览器只需要解析 html 不需要去解析 js。
How?SSR 的原理
借用下面的一张图,我们来简单阐述一下 vue-ssr 的原理。
我们可以看到,左侧 Source 部分就是我们所编写的源代码,所有代码有一个公共入口,就是 app.js,紧接着就是服务端的入口
(entry-server.js)和客户端的入口(entry-client.js)。当完成所有源代码的编写之后,我们通过 webpack 的构建,打包出两个 bundle,分别是 server bundle 和 client bundle;当用户进行页面访问的时候,先是经过服务端的入口,将 vue 组建组装为 html 字符串,并混入客户端所访问的 html 模板中,最终就完成了整个 ssr 渲染的过程。
开始搭建
创建一个空白目录并初始化
在终端输入以下命令
mkdir ssr-demo
cd ssr-demo
npm init
由于我们这个只是一个 demo 项目,可以直接一路按回车键,直接忽略配置。
完成之后我们可以看到文件夹里面有一个 package.json 的文件,这就是配置表。
安装依赖
该项目需要四个依赖,依次安装
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 中已经把四个依赖都写上了。
"express": "^4.17.1",
"vue": "^2.6.10",
"vue-router": "^3.0.6",
"vue-server-renderer": "^2.6.10"
创建一个 node 服务
在根目录下我们新建一个 server.js,用户搭建 node 服务
const express = require("express");
const app = express();
app.get('*', (request, response) => {response.end('hello, ssr');
})
app.listen(3001, () => {console.log('服务已开启')
})
接着为了后续开发的便利,我们在 package.json 中添加一个启动命令:
"scripts": {
"test": "echo \"Error: no test specified\"&& exit 1",
"server": "node index.js"
},
接着我们在终端输入 npm run server,然后再浏览器输入 localhost:3001,便可以看到页面中的文字被成功渲染。
渲染 html 页面
在上一步我们已经能成功渲染出一个文字,但是 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 标签等。
将 Vue 实例挂载进 html 模板中
创建一个 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")
});
保存代码,重启服务,然后重新刷新页面。我们查看源代码,发现,已经能成功渲染出一个完整的页面了。
创建一个 Vue 项目的开发目录
上面的开发模式,很显然只是一个 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 框架。
项目源码
https://github.com/TheWalking…