前言
许久不见,已是三月。春天是一个充满希望的季节~
前段时间看到一个关于服务端渲染的应用系统,于是对此产生了好奇。花了一些时间研究它的架构并且通过实践产生一个开发系统的demo。这篇文章就是对此次实践进行总结,以便之后在公司项目开发过程中如若有用到能快速回忆技术栈并且产出价值。
服务端渲染 & 浏览器端渲染
首先需要搞清两个概念:什么是浏览器端渲染?什么是服务端渲染?
浏览器端渲染(CRS:Client Side Render)页面上的内容是加载的js文件渲染出来的,js文件运行在浏览器上面,服务器只返回一个html模板。
服务器端渲染(SSR:Server Side Render)页面上的内容是通过服务器端渲染生成的,浏览器直接显示服务端返回的html即可。
Vue.js 是一个构建客户端应用的框架。默认情况下,作为其输出,Vue 组件会在浏览器中生产并封装 DOM。然而,我们也可以在服务器端把同样的组件渲染成 HTML 字符串,然后直接将其发送给浏览器,并最终将静态标记“激活”为完整的、可交互的客户端应用。
服务端渲染解决什么
SSR主要解决2个问题:
- 搜索引擎优先爬取页面的html结构,使用ssr服务端已经生成相关的html,有利于seo。
- 首屏呈现渲染速度,用户无需等待页面所有js加载完成就可以看到页面视图。当然这时候压力来到服务器,需要权衡。
在此基础上,我脑中想到了预渲染这种方式也是能够提升seo。区别的是预渲染是在构建项目进行打包时的时候会按照路由层级进行动态渲染对应的html文件,它和服务端渲染不同之处在于服务端渲染是在请求时渲染页面。
如何搭建服务端渲染
现在需要知道的是通过服务端渲染具体需要做些什么?通过阅读 Vue.js
服务端渲染文档,我总结了以下大方向的实现思路:
-
创建根实例
利用Vue提供的服务端渲染API
createSSRApp
来创建根实例。 -
需要一个服务端入口,每次请求都会创建一个新的根实例并且每次请求都会将应用实例渲染成一个字符串,返回HTML字符串。
通过
@vue/server-renderer
提供的renderToString
API将实例渲染成一个字符串。 -
需要一个客户端入口,将应该挂载到DOM上。
-
客户端激活
客户端激活指的是 Vue 接管由服务端发送的静态 HTML,并使其变为可以响应客户端数据变化的动态 DOM 的过程。
在实践的过程中,我使用vite
& vue3
技术栈搭建基础工程。利用vite
命令快速搭建:
npm create @vitejs/app vite-project
通过命令就搭建了一个基础工程,目录结构如下:
接下来编写核心代码,这里我们需要考虑vue-router
&vuex
的集成以及如何数据预取。因为是服务端渲染,所以在服务端渲染之前就要获取数据,要不然就没什么意义。
- main.ts
此文件是用来生成应该根实例,没什么好说的,就直接上代码吧。
// 生成vue实例
export function createApp(){
const app =createSSRApp(App);
const router =createRouter();
const store=createStore();
app.use(router).use(store)
return {app,router,store};
}
-
server.js 创建服务器
通过
express
搭建一个简易的服务器。阅读vite
官方文档,对此进行改造。在编码的过程中区别生产环境和开发环境。服务器主要是响应由vue执行后的html字符串:
app.use('*', async (req, res) => {
const url = req.originalUrl;
try {
let template,render;
if(isProd){
template = indexHtml;
render = require('./dist/server/entry-server.js').render();
}else{
// 1. 读取 index.html
template = fs.readFileSync( path.resolve(__dirname, 'index.html'),'utf-8')
// 2. 应用 Vite HTML 转换
template = await vite.transformIndexHtml(url, template);
// 3. 加载服务器入口。
render = (await vite.ssrLoadModule('/src/entry-server.ts')).render
}
// 4. 渲染应用的 HTML
const {appHtml,links,state} = await render(url,manifest,req);
// 5. 注入渲染后的应用程序 HTML 到模板中。
const html = template
.replace(`<!-- app-preload-links -->`,links)
.replace(`<!-- app-script -->`,`
<script type="application/javascript">window.__IS_FROM_SSR__=true;window.__INIT_STATE__=${state}</script>
`)
.replace(`<!--ssr-html-->`, appHtml);
// 6. 返回渲染后的 HTML。
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
vite.ssrFixStacktrace(e)
res.status(500).end(e.message)
}
})
-
entry-server.ts 服务端入口,数据预取以及资源加载都是在这里编码。
这里的数据预取实现方案是通过每个组件暴露一个hook方法来实现,也可以将获取数据的方法都集成到状态管理里,通过调用全局的方法来实现。
在挂载到应用之前需要保持客户端和服务端数据相同,否则会导致混合失败,也就是说客户端也需要做相同的获取数据的过程。
- entry-client.ts
别忘记客户端激活,所以需要改造index.html入口:
- package.json
最后,改造一下运行命令行:
对于服务端渲染,只有beforeCreate
和created
会在服务端渲染过程中被调用。其他的生命周期钩子函数中的代码,只会在客户端执行。
思考
通过阅读文档以及参考其他的架构快速产出demo是快速上手学习的方式,但如果想要更深入理解需要去学习源码或者说内部的编译原理。
此demo是最简易的,后期用于项目中还需要考虑如何优化项目,并且要考虑编码过程库在客户端 & 服务端兼容性问题。
demo地址:
参考文档: