客户端通过 HTTP 向服务器请求资源,当必要的资源都加载完毕后,执行 new Vue() 开始实例化并渲染页面。
服务端渲染过程
访问服务端渲染的网站。
服务器会查看当前路由组件需要哪些资源文件,然后将这些文件的内容填充到 HTML 文件。如果有 asyncData() 函数,就会执行它进行数据预取并填充到 HTML 文件里,最后返回这个 HTML 页面。
当客户端接收到这个 HTML 页面时,可以马上就开始渲染页面。与此同时,页面也会加载资源,当必要的资源都加载完毕后,开始执行 new Vue() 开始实例化并接管页面。
从上述两个过程中,可以看出,区别就在于第二步。客户端渲染的网站会直接返回 HTML 文件,而服务端渲染的网站则会渲染完页面再返回这个 HTML 文件。
这样做的好处是什么?是更快的内容到达时间 (time-to-content)。
假设你的网站需要加载完 abcd 四个文件才能渲染完毕。并且每个文件大小为 1 M。
这样一算:客户端渲染的网站需要加载 4 个文件和 HTML 文件才能完成首页渲染,总计大小为 4M(忽略 HTML 文件大小)。而服务端渲染的网站只需要加载一个渲染完毕的 HTML 文件就能完成首页渲染,总计大小为已经渲染完毕的 HTML 文件(这种文件不会太大,一般为几百K,我的个人博客网站(SSR)加载的 HTML 文件为 400K)。这就是服务端渲染更快的原因。
客户端接管页面
对于服务端返回来的 HTML 文件,客户端必须进行接管,对其进行 new Vue() 实例化,用户才能正常使用页面。
建议先阅读官方指南——Vue.js 服务器端渲染指南,再回到本文开始阅读。
本文将分成以下两部分:
好了,下面开始正文。
简述 Vue SSR 过程
客户端渲染过程
<div id="app"></div>
的 HTML 文件。new Vue()
开始实例化并渲染页面。服务端渲染过程
asyncData()
函数,就会执行它进行数据预取并填充到 HTML 文件里,最后返回这个 HTML 页面。new Vue()
开始实例化并接管页面。从上述两个过程中,可以看出,区别就在于第二步。客户端渲染的网站会直接返回 HTML 文件,而服务端渲染的网站则会渲染完页面再返回这个 HTML 文件。
这样做的好处是什么?是更快的内容到达时间 (time-to-content)。
假设你的网站需要加载完 abcd 四个文件才能渲染完毕。并且每个文件大小为 1 M。
这样一算:客户端渲染的网站需要加载 4 个文件和 HTML 文件才能完成首页渲染,总计大小为 4M(忽略 HTML 文件大小)。而服务端渲染的网站只需要加载一个渲染完毕的 HTML 文件就能完成首页渲染,总计大小为已经渲染完毕的 HTML 文件(这种文件不会太大,一般为几百K,我的个人博客网站(SSR)加载的 HTML 文件为 400K)。这就是服务端渲染更快的原因。
客户端接管页面
对于服务端返回来的 HTML 文件,客户端必须进行接管,对其进行
new Vue()
实例化,用户才能正常使用页面。如果不对其进行激活的话,里面的内容只是一串字符串而已,例如下面的代码,点击是无效的:
那客户端如何接管页面呢?下面引用一篇文章中的内容:
小结
不管是客户端渲染还是服务端渲染,都需要等待客户端执行
new Vue()
之后,用户才能进行交互操作。但服务端渲染的网站能让用户更快的看见页面。从零开始搭建 SSR 项目
配置 weback
webpack 配置文件共有 3 个:
webpack.base.config.js
,基础配置文件,客户端与服务端都需要它。webpack.client.config.js
,客户端配置文件,用于生成客户端所需的资源。webpack.server.config.js
,服务端配置文件,用于生成服务端所需的资源。webpack.base.config.js
基础配置文件基础配置文件比较简单,
output
属性的意思是打包时根据文件内容生成文件名称。module
属性配置不同文件的解析 loader。webpack.client.config.js
客户端配置文件客户端配置文件中的
config.optimization
属性是打包时分割代码用的。它的作用是将第三方库都打包在一起。其他插件作用:
MiniCssExtractPlugin
插件, 将 css 提取出来单独打包。CssMinimizerPlugin
插件,压缩 css。CompressionPlugin
插件,将资源压缩成 gzip 格式(大大提升传输效率)。另外还需要在 node 服务器上引入compression
插件配合使用。WebpackBar
插件,打包时显示进度条。webpack.server.config.js
服务端配置文件服务端打包和客户端不同,它将所有文件一起打包成一个文件
server-bundle.js
。同时解析 css 需要使用vue-style-loader
,这一点在官方指南中有说明:配置服务器
生产环境
pro-server.js
生产环境服务器配置文件从代码中可以看到,当首次加载页面时,需要调用
createBundleRenderer()
生成一个 renderer,它的参数是打包生成的vue-ssr-server-bundle.json
和vue-ssr-client-manifest.json
文件。当返回 HTML 文件后,页面将会被客户端接管。在文件的最后有一行代码
app.get('*', render)
,它表示所有匹配不到的请求都交给它处理。所以如果你写了 ajax 请求处理函数必须放在前面,就像下面这样:否则你的页面会打不开。
开发环境
开发环境的服务器配置和生产环境没什么不同,区别在于开发环境下的服务器有热更新。
一般用 webpack 进行开发时,简单的配置一下 dev server 参数就可以使用热更新了,但是 SSR 项目需要自己配置。
由于 SSR 开发环境服务器的配置文件
setup-dev-server.js
代码太多,我对其进行简化后,大致代码如下:同时需要在 webpack 的入口文件加上这一行代码
webpack-hot-middleware/client?reload=true
。然后使用
node dev-server.js
来开启前端代码热更新。热更新主要使用了两个插件:
webpack-dev-middleware
和webpack-hot-middleware
。顾名思义,看名称就知道它们的作用,webpack-dev-middleware
的作用是生成一个与 webpack 的 compiler 绑定的中间件,然后在 express 启动的 app 中调用这个中间件。webpack-hot-middleware
插件的作用就是热更新,它需要配合HotModuleReplacementPlugin
和webpack-dev-middleware
一起使用。打包文件
vue-ssr-client-manifest.json
和vue-ssr-server-bundle.json
webpack 需要对源码打包两次,一次是为客户端环境打包的,一次是为服务端环境打包的。
为客户端环境打包的文件,和以前我们打包的资源一样,不过多出了一个
vue-ssr-client-manifest.json
文件。服务端环境打包只输出一个vue-ssr-server-bundle.json
文件。vue-ssr-client-manifest.json
包含了客户端环境所需的资源名称:从上图中可以看到有三个关键词:
vue-ssr-server-bundle.json
文件:填坑记录
1.
[vue-router] failed to resolve async component default: referenceerror: window is not defined
由于在一些文件或第三方文件中可能会用到 window 对象,并且 node 中不存在 window 对象,所以会报错。 此时可在
src/app.js
文件加上以下代码进行判断:2.
mini-css-extract-plugin
插件造成ReferenceError: document is not defined
使用
mini-css-extract-plugin
插件打包的的server bundle
, 会使用到 document。由于 node 环境中不存在 document 对象,所以报错。解决方案:样式相关的 loader 不要放在
webpack.base.config.js
文件,将其分拆到webpack.client.config.js
和webpack.client.server.js
文件。其中mini-css-extract-plugin
插件要放在webpack.client.config.js
文件配置。base
client
server
3. 开发环境下跳转页面样式不生效,但生产环境正常。
由于开发环境使用的是
memory-fs
插件,打包文件是放在内存中的。如果此时dist
文件夹有刚才打包留下的资源,就会使用dist
文件夹中的资源,而不是内存中的资源。并且开发环境和打包环境生成的资源名称是不一样的,所以就造成了这个 BUG。解决方法是执行
npm run dev
时,删除dist
文件夹。所以要在npm run dev
对应的脚本中加上rimraf dist
。4.
[vue-router] Failed to resolve async component default: ReferenceError: document is not defined
不要在有可能使用到服务端渲染的页面访问 DOM,如果有这种操作请放在
mounted()
钩子函数里。如果你引入的数据或者接口有访问 DOM 的操作也会报这种错,在这种情况下可以使用
require()
。因为require()
是运行时加载的,所以可以这样使用:修改后:
修改后可以正常使用。
5. 开发环境下,开启服务器后无任何反应,也没见控制台输出报错信息。
这个坑其实是有报错信息的,但是没有输出,导致以为没有错误。
在
setup-dev-server.js
文件中有一行代码if (stats.errors.length) return
,如果有报错就直接返回,不执行后续的操作。导致服务器没任何反应,所以我们可以在这打一个console.log
语句,打印报错信息。小结
这个 DEMO 是基于官方 DEMO vue-hackernews-2.0 改造的。不过官方 DEMO 发表于 4 年前,最近修改时间是 2 年前,很多选项参数已经过时了。并且官方 DEMO 需要翻墙才能使用。所以我在此基础上对其进行了改造,改造后的 DEMO 放在 Github 上,它是一个比较完善的 DEMO,可以在此基础上进行二次开发。
如果你不仅仅满足于一个 DEMO,建议看一看我的个人博客项目,它原来是客户端渲染的项目,后来重构为服务端渲染,绝对实战。
参考资料
更多文章,敬请关注