服务端渲染(SSR)和浏览器端渲染 (CSR)

转载服务端渲染(SSR) - 知乎 (zhihu.com)

一、什么是浏览器端渲染 (CSR)?

CSR是Client Side Render简称;页面上的内容是我们加载的js文件渲染出来的,js文件运行在浏览器上面,服务端只返回一个html模板。


CSR加载图

二、什么是服务器端渲染 (SSR)?

SSR是Server Side Render简称;页面上的内容是通过服务端渲染生成的,浏览器直接显示服务端返回的html就可以了。


SSR加载图

本文以Vue.js 做为演示框架来区分SSR和CSR。默认情况下,Vue.js可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。

服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。

附:vue-ssr官方文档

基本用法 | Vue SSR 指南ssr.vuejs.org/zh/guide/

三、不同渲染方式在浏览器解析情况

从输入页面URL到页面渲染完成大致流程为:

解析URL

浏览器本地缓存

DNS解析

建立TCP/IP连接

发送HTTP请求

服务器处理请求并返回HTTP报文

浏览器根据深度遍历的方式把html节点遍历构建DOM树

遇到CSS外链,异步加载解析CSS,构建CSS规则树

遇到script标签,如果是普通JS标签则同步加载并执行,阻塞页面渲染,如果标签上有defer / async属性则异步加载JS资源

将dom树和CSS DOM树构造成render树

渲染render树


performance.timing


CSR-浏览器performance情况


SSR-浏览器performance情况

FP:首次绘制。用于标记导航之后浏览器在屏幕上渲染像素的时间点。这个不难理解,就是浏览器开始请求网页到网页首帧绘制的时间点。这个指标表明了网页请求是否成功。

FCP:首次内容绘制。FCP 标记的是浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至  元素。

FMP:首次有效绘制。这是一个很主观的指标。根据业务的不同,每一个网站的有效内容都是不相同的,有效内容就是网页中"主角元素"。对于视频网站而言,主角元素就是视频。对于搜索引擎而言,主角元素就是搜索框。

TTI:可交互时间。用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点。应用可能会因为多种原因而无法响应用户输入:①页面组件运行所需的JavaScript尚未加载完成。②耗时较长的任务阻塞主线程

根据上图devtool时间轴的结果,虽然CSR配合预渲染方式(loading、骨架图)可以提前FP、FCP从而减少白屏问题,但无法提前FMP;SSR将FMP提前至js加载前触发,提前显示网页中的"主角元素"。SSR不仅可以减少白屏时间还可以大幅减少首屏加载时间。

附:首屏时间获取方法

前端 白屏时间如何获取?18 赞同 · 9 评论回答

四、node服务(server.js)

第一步 利用express框架写一个简单node服务

Express是基于Node.js平台,快速、开放、极简的 Web 开发框架

/*第一步 利用express框架写一个简单node服务*/letexpress=require('express');letapp=express();app.get('*',function(req,res){res.send('hello world');});constport=process.env.PORT||8080app.listen(port,()=>{console.log(`server started at localhost:${port}`)})

附:express文档

Express - 基于 Node.js 平台的 web 应用开发框架www.expressjs.com.cn/

第二步 利用vue-server-renderer提供的createRenderer将vue与node结合

renderer.renderToString(vm, context?, callback?): ?Promise

将 Vue 实例渲染为字符串。上下文对象 (context object) 可选。回调函数是典型的 Node.js 风格回调,其中第一个参数是可能抛出的错误,第二个参数是渲染完毕的字符串。

/*第一步 利用express框架写一个简单node服务第二步 利用vue-server-renderer提供的createRenderer将vue与node结合*/

let express=require('express');

let app=express();

constVue=require('vue')constrenderer=require('vue-server-renderer').createRenderer()app.get('*',function(req,res){render(req,res)});functionrender(req,res){constapp=newVue({data:{url:req.url},template:`

req.url:{{ url }}
`})renderer.renderToString(app,(err,html)=>{if(err){res.status(500).end('Internal Server Error')return}else{res.end(`${html}`)}})}constport=process.env.PORT||8080app.listen(port,()=>{console.log(`server started at localhost:${port}`)})

第三步 读入index.template.html文件

创建 renderer 时提供一个页面模板。多数时候,我们会将页面模板放在特有的文件中,例如index.template.html

Hello

注释 -- 这里将是应用程序 HTML 标记注入的地方。

/*第一步 利用express框架写一个简单node服务第二步 利用vue-server-renderer提供的createRenderer将vue与node结合第三步 读入index.template.html文件*/letexpress=require('express');letapp=express();constVue=require('vue')constpath=require('path')constresolve=file=>path.resolve(__dirname,file)constrenderer=require('vue-server-renderer').createRenderer({template:require('fs').readFileSync(resolve('./src/index.template.html'),'utf-8')})app.get('*',function(req,res){render(req,res)});functionrender(req,res){constapp=newVue({data:{url:req.url},template:`

req.url:{{ url }}
`})constcontext={title:'ssr测试',}renderer.renderToString(app,context,(err,html)=>{if(err){res.status(500).end('Internal Server Error')return}else{res.end(`${html}`)}})}constport=process.env.PORT||8080app.listen(port,()=>{console.log(`server started at localhost:${port}`)})

第四步 引入已经打包好的vue-ssr-server-bundle.json

vue-server-renderer 提供一个名为 createBundleRenderer 的 API,用于处理此问题,通过使用 webpack 的自定义插件,server bundle 将生成为可传递到 bundle renderer 的特殊 JSON 文件。所创建的 bundle renderer,用法和普通 renderer 相同,但是 bundle renderer 提供以下优点:

const renderer = createBundleRenderer(serverBundle, {

  runInNewContext: false, // 推荐,bundle 代码将与服务器进程在同一个 global 上下文中运行

  template, // (可选)页面模板

  clientManifest // (可选)客户端构建 manifest

})

内置的 source map 支持(在 webpack 配置中使用 devtool: 'source-map')

在开发环境甚至部署过程中热重载(通过读取更新后的 bundle,然后重新创建 renderer 实例)

关键 CSS(critical CSS) 注入(在使用 *.vue 文件时):自动内联在渲染过程中用到的组件所需的CSS。

使用 clientManifest 进行资源注入:自动推断出最佳的预加载(preload)和预取(prefetch)指令,以及初始渲染所需的代码分割 chunk。

/*第一步 利用express框架写一个简单node服务第二步 利用vue-server-renderer提供的createRenderer将vue与node结合第三步 读入index.template.html文件第四步 引入已经打包好的vue-ssr-server-bundle.json*/letexpress=require('express');letapp=express();constpath=require('path')constresolve=file=>path.resolve(__dirname,file)consttemplatePath=resolve('./src/index.template.html')constserverBundle=require('./dist/vue-ssr-server-bundle.json')const{createBundleRenderer}=require('vue-server-renderer')letrenderer=createBundleRenderer(serverBundle,{template:require('fs').readFileSync(templatePath,'utf-8'),//clientManifest 客户端构建 manifest 暂不演示})app.get('*',function(req,res){render(req,res)});functionrender(req,res){constcontext={title:'ssr测试',url:req.url// 传递path,这个参数很重要}renderer.renderToString(context,(err,html)=>{if(err){res.status(500).end('Internal Server Error')return}else{res.end(`${html}`)}})}constport=process.env.PORT||8080app.listen(port,()=>{console.log(`server started at localhost:${port}`)})

第五步 将bundle换成webpack实时输入的内存的bundle(非生产环境)

webpack 默认使用普通文件系统来读取文件并将文件写入磁盘。但是,还可以使用不同类型的文件系统(内存(memory), webDAV 等)来更改输入或输出行为。为了实现这一点,可以改变inputFileSystem或outputFileSystem

调用watch方法会触发 webpack 执行器,但之后会监听变更(很像 CLI 命令:webpack --watch),一旦 webpack 检测到文件变更,就会重新执行编译。该方法返回一个Watching实例。

/*第一步 利用express框架写一个简单node服务第二步 利用vue-server-renderer提供的createRenderer将vue与node结合第三步 读入index.template.html文件第四步 引入已经打包好的vue-ssr-server-bundle.json第五步 将bundle换成webpack实时输入的内存的bundle*/letexpress=require('express');letapp=express();constpath=require('path')constresolve=file=>path.resolve(__dirname,file)consttemplatePath=resolve('./src/index.template.html')//const bundle = require('./dist/vue-ssr-server-bundle.json')constwebpack=require('webpack')constserverConfig=require('./build/webpack.server.config')constMFS=require('memory-fs')constreadFile=(fs,file)=>{try{returnfs.readFileSync(path.join(serverConfig.output.path,file),'utf-8')}catch(e){}}const{createBundleRenderer}=require('vue-server-renderer')letrenderer;app.get('*',function(req,res){render(req,res)});functionrender(req,res){constcontext={title:'ssr测试',url:req.url// 传递path,这个参数很重要}renderer.renderToString(context,(err,html)=>{if(err){res.status(500).end('Internal Server Error')return}else{res.end(`${html}`)}})}constserverCompiler=webpack(serverConfig)constmfs=newMFS()serverCompiler.outputFileSystem=mfs//打包至内存中serverCompiler.watch({},(err,stats)=>{if(err)throwerrletbundle=JSON.parse(readFile(mfs,'vue-ssr-server-bundle.json'))renderer=createBundleRenderer(bundle,{template:require('fs').readFileSync(templatePath,'utf-8'),})})constport=process.env.PORT||8080app.listen(port,()=>{console.log(`server started at localhost:${port}`)})

附:webpack在Node.js 中的API

Node.js API | webpack 中文网www.webpackjs.com/api/node/

五、剖析构建流程

构建流程

通用配置(Base Config)

服务器配置 (Server Config)

服务器配置,是用于生成传递给 createBundleRenderer 的 server bundle。它应该是这样的:

constmerge=require('webpack-merge')constnodeExternals=require('webpack-node-externals')constbaseConfig=require('./webpack.base.config.js')constVueSSRServerPlugin=require('vue-server-renderer/server-plugin')module.exports=merge(baseConfig,{// 将 entry 指向应用程序的 server entry 文件entry:'/path/to/entry-server.js',// 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),// 并且还会在编译 Vue 组件时,// 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。target:'node',// 对 bundle renderer 提供 source map 支持devtool:'source-map',// 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)output:{libraryTarget:'commonjs2'},// https://webpack.js.org/configuration/externals/#function// https://github.com/liady/webpack-node-externals// 外置化应用程序依赖模块。可以使服务器构建速度更快,// 并生成较小的 bundle 文件。externals:nodeExternals({// 不要外置化 webpack 需要处理的依赖模块。// 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,// 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单whitelist:/\.css$/}),// 这是将服务器的整个输出// 构建为单个 JSON 文件的插件。// 默认文件名为 `vue-ssr-server-bundle.json`plugins:[newVueSSRServerPlugin()]})

在生成 vue-ssr-server-bundle.json 之后,只需将文件路径传递给 createBundleRenderer:

const{createBundleRenderer}=require('vue-server-renderer')constrenderer=createBundleRenderer('/path/to/vue-ssr-server-bundle.json',{// ……renderer 的其他选项})

客户端配置 (Client Config)

除了 server bundle 之外,我们还可以生成客户端构建清单 (client build manifest)。使用客户端清单 (client manifest) 和服务器 bundle(server bundle),renderer 现在具有了服务器和客户端的构建信息,因此它可以自动推断和注入资源预加载 / 数据预取指令(preload / prefetch directive),以及 css 链接 / script 标签到所渲染的 HTML。

好处是双重的:

在生成的文件名中有哈希时,可以取代 html-webpack-plugin 来注入正确的资源 URL。

在通过 webpack 的按需代码分割特性渲染 bundle 时,我们可以确保对 chunk 进行最优化的资源预加载/数据预取,并且还可以将所需的异步 chunk 智能地注入为 

你可能感兴趣的:(服务端渲染(SSR)和浏览器端渲染 (CSR))