渲染过程 浏览器的渲染过程

我主要从个人角度介绍一下我对服务器端渲染的理解。阅读本文后,您将了解到:

什么是服务器端渲染,它和客户端渲染有什么区别?为什么需要服务器端渲染?服务器端渲染的优缺点是什么?如何同构VUE项目?服务器端渲染的定义在讲服务级渲染之前,我们先来回顾一下页面的渲染过程:

浏览器请求html文本呈现过程来解析HTML文本,并构建DOM树来解析HTML。同时,如果遇到内联样式或样式脚本,会下载并构建stytle规则,如果遇到Javascript脚本,会下载并执行脚本。在构建DOM树和样式规则之后,呈现过程将它们合并到一个呈现树中。渲染过程开始布局渲染树,生成布局树,绘制布局树,并生成绘图记录。渲染过程将布局树分层,光栅化每一层,并获得合成帧。渲染进程将复合帧信息发送到GPU进程以显示在页面上17afaefa40674a8cb7fdb49c4b56371c?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=eWE4qXlYfzE3HAEXXdqwJPs1a7o%3D&index=0

可以看到,页面的渲染其实就是浏览器将HTML文本转换成页面框架的过程。现在我们大部分的WEB应用都是使用JavaScript框架(Vue,React,Angular)来渲染页面。也就是说,JavaScript脚本执行的时候,HTML页面已经解析好了,DOM树已经构建好了。JavaScript脚本只是动态地改变DOM树的结构,使页面变成它想要的样子。这种渲染方式叫动态渲染,也可以叫客户端仁德。

那么什么是服务器端渲染呢?顾名思义,服务器端渲染就是当浏览器请求页面的URL时,服务器把我们需要的HTML文本组装起来,返回给浏览器。浏览器解析HTML文本后,不需要执行JavaScript脚本,就可以直接构造想要的DOM树并显示在页面上。这种在服务器端组装HTML的过程称为服务器端呈现。

8fc9948acd2f4e62896eb39c4a97c9a3?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=yUUjsgQYtE0SpcwDaxkHQWJaoGI%3D&index=1

服务器端渲染的起源WEB1.0在没有AJAX的时候,也就是Web1.0时代,几乎所有的应用都是服务器端渲染(此时的服务器渲染并不是现在的服务器渲染)。当时的页面渲染大概是这样的。浏览器请求页面URL,然后服务器收到请求后,去数据库查询数据,把数据扔进后端组件模板(php,asp,jsp等。).并将其渲染成HTML片段,然后服务器将这些HTML片段组装起来形成一个完整的HTML,最后返回给浏览器。此时浏览器已经得到了一个完整的由服务器动态组装的HTML文本,然后将HTML渲染到页面中,这个过程中没有任何JavaScript代码参与。

f3ca20bdd1de41fdbdb6653874c5f0ba?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=BS%2FldAoCJmQtAkanMBEvZc4uaQ8%3D&index=2

客户端渲染在WEB1.0时代,服务器端渲染似乎是当时最好的渲染方式,但是随着业务的日益复杂和后续AJAX的出现,WEB1.0服务器端渲染的缺点也逐渐暴露出来。

每次更新页面的一个小模块,都需要再次请求页面,再次检查数据库,重新组装HTML。前端JavaScript代码和后端(jsp,php,jsp)代码混杂在一起,使得日益复杂的WEB应用难以维护。那时候根本没有前端工程师的职位,前端的js工作一般都是后端的同学jQuery来承担。但是随着前端页面逐渐复杂,后端开始觉得js麻烦。虽然很简单,但是漏洞太多,公司就招了一些专门写js的人,也就是前端。这时候前端就鄙视前端了,因为后端觉得js太简单了,无非就是写页面的特效(JS)和剪贴图(css)。这根本不是一个真正的程序员。

随着nodejs的出现,前端看到了翻身的机会。为了摆脱后端的指指点点,前端开始了一场前端与后端分离的运动,希望从后端独立发展。前端分离,表面上看起来是代码分离,实际上是为了前端人员的分离,也就是前端和后端团队的分离。

前端和后端分离后,网页开始被当作一个独立的应用(SPA,单页应用)。前端团队接管所有页面渲染,后端团队只负责提供所有数据查询和处理的API。大致流程如下:首先浏览器请求URL,前端服务器直接返回一个空的静态html文件(没有任何数据库查询和模板组装)。这个HTML文件加载了呈现页面所需的许多JavaScript脚本和CSS样式表。浏览器获得HTML文件后,开始加载脚本和样式表,并执行脚本。此时,脚本请求后端服务提供的API来获取数据。采集完成后,数据会通过JavaScript脚本动态渲染到页面,完成页面展示。

c049c515c96945acba6778941492fafc?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=P%2FsftBwmVKhM2KH3v%2FSvi66iMY4%3D&index=3

这种将正面和背面分开的渲染模式也称为客户端渲染(CSR)。

随着单页应用(SPA)的发展,程序员逐渐发现SEO(搜索引擎优化)有问题,而且随着应用的复杂,JavaScript脚本不断臃肿,导致首屏渲染比Web1.0中的服务器渲染慢很多。

选择自己的路,跪着走下去。于是前端团队选择用nodejs在服务器端渲染页面,然后服务器端渲染又出现了。通常,该过程类似于客户端渲染。首先,浏览器请求URL。前端服务器收到URL请求后,根据不同的URL向后端服务器请求数据。请求完成后,前端服务器会用特定的数据组装一个HTML文本,返回给浏览器。浏览器获取HTML后,开始呈现页面。同时,浏览器加载并执行JavaScript脚本,将事件绑定到页面上的元素,并使页面具有交互性。当用户与浏览器页面交互时,比如跳转到下一个页面,浏览器会执行JavaScript脚本,向后端服务器请求数据,获取数据后再次执行JavaScript代码动态渲染页面。

7372283b2a9743f8b50fc58386e65368?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=Gsj9%2BIGrgVUiAF7tN1g9mwlXUFI%3D&index=4

服务器端渲染的优缺点与客户端渲染相比,服务器端渲染的优点是什么?

对SEO好就是对SEO好。事实上,爬虫抓取你的页面是有好处的。那么,当别人用搜索引擎搜索相关内容时,你的网页排名可以更高,这样你的流量就会更高。那为什么服务器端渲染更有利于爬虫抓取你的页面呢?其实爬行动物可以分为低级爬行动物和高级爬行动物。

低级爬虫:只请求URL,爬行URL返回的任何HTML内容。高级爬虫:请求URL,加载并执行JavaScript脚本来呈现页面,爬行JavaScript呈现的内容。也就是说,底层爬虫对客户端渲染的页面无能为力,因为返回的HTML是一个空壳,需要执行一个JavaScript脚本才能渲染出真正的页面。目前像百度、谷歌、微软等公司。一些老爬虫属于低级爬虫,通过使用服务器端渲染,对这些低级爬虫更加友好。

与客户端渲染相比,服务器端渲染在浏览器请求URL后,已经得到了一个带有数据的HTML文本。浏览器只需要解析HTML并直接构建DOM树。对于客户端呈现,首先需要获得一个空的HTML页面。此时页面已经进入白屏,然后需要加载并执行JavaScript,请求后端服务器获取数据,JavaScript渲染页面,才能看到最终页面。尤其是在复杂的应用中,由于需要加载JavaScript脚本,应用越复杂,需要加载的JavaScript脚本越多越大,会导致应用首屏加载时间非常长,从而降低体验。

6f98206e80fd491197a98c82bd83783e?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=O%2B6N7e0G%2FY7WW5y2GP2O1rE9KD8%3D&index=5

服务器端渲染的缺点不是所有的WEB应用都必须使用SSR,这个需要开发者自己权衡,因为服务器端渲染会带来以下问题:

代码复杂性增加。为了实现服务器端渲染,应用程序代码需要同时兼容服务器端和客户端,而一些依赖的外部扩展库只能在客户端运行,需要特殊处理才能在服务器端渲染应用中运行。需要更多的服务器负载平衡。由于服务器增加了渲染HTML的需求,原本只需要输出静态资源文件的nodejs服务增加了数据采集的IO和渲染HTML的CPU。如果流量突然激增,可能会导致服务器关闭,因此需要使用响应式缓存策略,并准备相应的服务器负载。与构建设置和部署相关的更多要求。与可以部署在任何静态文件服务器上的完全静态的单页应用程序(SPA)不同,服务器需要在Node.js服务器的运行环境中才能呈现应用程序。所以在使用服务器渲染SSR之前,开发者需要考虑投入产出比。比如大部分应用系统不需要SEO,首屏时间也不是很慢。如果用SSR,那就小题大做了。

知道了服务器渲染同构的优缺点,如果我们需要在项目中使用服务器渲染,我们需要做什么?那就是同构我们的项目。

同构是在服务器端渲染中定义的。页面呈现有两种方式:

前端服务器请求后端服务器获取数据,组装html返回给浏览器,浏览器直接解析html然后渲染页面。在交互过程中,浏览器请求新数据并动态更新呈现的页面。这两种渲染方式的一个区别是,一种是在服务器端组装HTML,一种是在客户端组装HTML,运行环境不同。所谓同构,就是一段代码在服务端和客户端都可以执行,执行效果是一样的,就是完成这个html的组装,正确显示页面。也就是说,一段代码可以由客户端渲染,也可以由服务器渲染。

同构的条件要达到同构需要满足哪些条件?首先,我们来思考一下一个应用中页面的构成。如果我们使用Vue.js,当我们打开一个页面时,首先打开的是这个页面的URL。此URL可用于通过应用程序的路由匹配来查找特定页面。不同的页面有不同的视图。那么,观点是什么?从应用的角度来说,视图=模板数据,那么在Vue.js中,模板可以理解为组件,数据可以理解为数据模型,也就是响应式数据。因此,对于同构应用,我们必须实现客户端和服务器之间的路由、模型组件和数据模型的共享。

7d047f2b53814a22b1e794c5bc72b831?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=N9IsExS6XtD9OW2k%2F53eUCL4Lrc%3D&index=6

了解了服务器端渲染和同构的原理后,我们从零开始,一步一步完成同构,通过实践了解SSR。

基本NODEJS服务器呈现。首先,模拟最简单的服务器渲染,只需将我们需要的html文件返回到页面。

const express=require(' express ');\ n const app=express();app.get('/'),function(req,RES){ \ n RES . send(`\ n html \ n head \ n titles Sr/title \ n/head \ n body \ n phello world/p \ n/body \ n/html \ n `);\ n });app.listen(3001,function(){ \ n console . log(' listen:3001 ');\ n });启动后打开localhost:3001可以看到页面显示hello world。并打开网页源代码:

e6c129a9b0404132894cf29a0bfd1853?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=fxJsMtjlfJaAqlOGvx5lkHVfiyg%3D&index=7

也就是说,浏览器在获取服务器返回的HTML源代码时,可以直接显示hello world,而不需要加载任何JavaScript脚本。

基本VUE客户端渲染的实现我们使用vue-cli创建一个新的vue项目,并修改一个App.vue组件:

template \ n \ tdiv \ n \ t \ tphello world/p \ n \ t \ t button @ click=' say hello ' say hello/button \ n \ t/div \ n/template \ n \ n script \ n export default { \ n方法:{ \ n say hello(){ \ n \ t alert(' hello SSR '); } }}/script然后运行npm run serve启动项目并打开浏览器。你也可以看到页面显示hello world,但是打开我们网页的源代码:

7c502f74c3f247bab73a3cdddf28c219?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=pB82YOWBMcKWl%2FTKOq5Io8ENl5Y%3D&index=8

除了简单的兼容性处理noscript标签外,只有一个简单的id为app的div标签,没有关于hello world的字。可以说这是一个空页面(白屏)。当加载了下面Script标签的JavaScript脚本后,页面开始运行这些脚本,执行后,hello world正常显示。也就是说,真正渲染hello world的是JavaScript脚本。

同构Vue项目中配置模板组件的共享实际上是使用同一套组件代码。为了实现VUE组件可以在服务器上运行,我们首先需要解决代码编译的问题。通常,vue项目使用webpack来构建代码。同样,服务器端的代码可以通过使用webpack和借用一个官方的来构建。

88ccfe5521084daba6c9424f16d135c8?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=VzXSz01pzSJK%2B4tGCspTcNDeiXI%3D&index=9

步骤1:构建服务器端代码。从上图可以看出,服务器端代码构建完成后,需要在nodejs服务器上运行构建结果。但是,对于服务器端代码构建,有以下几点需要注意:

不需要编译CSS,只有在浏览器(客户端)运行时才需要样式表。构建目标的运行环境是commonjs,nodejs的模块化模式是commonjs,不需要代码裁剪。nodejs将所有代码一次性加载到内存中效率更高。因此,我们得到了服务器的webpack构建配置文件vue.server.config.js。

const node externals=require(' web pack-node-externals ');\ N const vuessserverplugin=require(' vue-server-renderer/server-plugin ')\ N \ N module . exports={ \ N CSS:{ \ N extract:false//不要提取CSS },\ N Configure Web Pack:()=({ \ N Entry:`。/src/server-entry . js `,//服务器条目文件\ n DevTool:' source-map '\ n Target:' node '//构建目标是nodejs环境\ n输出:{ \ library Target:' common js 2 '//构建目标加载模式commonjs }, //跳过node_mdoules,它将在运行时自动加载,而不编译\ n Externals:node Externals({ \ n allow list:[/\ \。css$/] //为了css模块的方便而允许CSS文件\ n}),\ n优化:{ \ nSplit Chunks:false//关闭代码切割\ n},\ n \ t插件:[\ nNewVuessServer Lugin () \ n] \ n})使用vue-server-renderer提供的服务器插件。这个插件主要和下面提到的客户端插件一起使用。主要用于实现开发过程中nodejs的热加载、源码图和html文件生成。

步骤2:构建客户端代码。当构建客户机代码时,使用客户机的执行入口文件。构建完成后,构建结果可以在浏览器中运行。但是,在服务器端呈现中,HTML是由服务器呈现的。也就是说,加载那些JavaScript脚本是由服务器决定的,因为HTML中的脚本标签是由服务器拼接的。所以在构建客户端代码的时候,我们需要使用插件生成一个构建结果列表,用来告诉服务器当前页面需要加载哪些JS脚本和CSS样式表。

因此,我们得到了客户端的构建配置vue.client.config.js

const VueSSRClientPlugin=require(' vue-server-renderer/client-plugin ')\ n \ n模块。exports={ \ n configure web pack:()=({ \ n entry:` ./src/client-entry.js ' devtool: 'source-map ' target: 'web '\ n plugins:[\ n new VueSSRClientPlugin()\ n]\ n }), chainWebpack: config={ //去除所有关于客户端生成的超文本标记语言配置,因为已经交给后端生成\ n配置。插件。删除(' html ');\ n配置。插件。删除(' preload '). config.plugins.delete('预取');\ n } \ n }使用服务器渲染器提供的客户端-服务器,主要作用是生成构建加过清单vue-ssr-client-manifest.json,服务端在渲染页面时,根据这个清单来渲染超文本标记语言中的脚本标签(JavaScript)和环标签(CSS)。

接下来,我们需要将vue。客户端。配置文件和vue.server.config.js都交给vue-cli内置的构建配置文件vue.config.js,根据环境变量使用不同的配置

//vue。配置。js \ n const TARGET _ NODE=process。环境。web pack _ TARGET===' NODEconst serverConfig=require('/vue。服务器。config’);const clientConfig=require('/vue。客户。config’);\ n \ n if(目标节点){ \ n模块。exports=服务器配置;其他模块。exports=客户端配置;}使用跨环境区分环境

'脚本''服务器''巴别节点src/服务器。' js '\ n ' serve '' vue-CLI-service serve '\ n ' build '' vue-CLI-service build '\ n ' build:server '' cross-env web pack _ TARGET=node vue-CLI-service build-mode server ' \ n } \ n }模板组件共享第一步:创建VUE实例为了实现模板组件共享,我们需要将获取某视频剪辑软件渲染实例写成通用代码,如下createApp:

从“Vue”导入Vue\ n从'导入应用程序./App '\ n \ n导出默认函数create App(context){ \ n const App=new Vue({ \ n render:h=h(App)\ n });\ n \ t返回{ \ n \ tapp \ n }\ n };第二步:客户端实例化VUE新建客户端项目的入口文件,客户端条目。射流研究…

从“Vue”导入Vue \ n导入从以下位置创建应用程序./创建应用程序'\ n \ n const { app }=create app();\ n \应用程序.$ mount(' # app ');客户端条目。射流研究…是浏览器渲染的入口文件,在浏览器加载了客户端编译后的代码后,组件会被渲染到身份证明(识别)为应用的元素节点上。

第三步:服务端实例化VUE新建服务端代码的入口文件,服务器条目。射流研究…

从'导入创建应用程序./创建应用程序' \ n \ n导出默认上下文={ \ n const { app }=创建应用程序(上下文);返回应用程序;\ n }服务器条目。射流研究…是提供给服务器渲染某视频剪辑软件组件的入口文件,在浏览器通过统一资源定位器访问到服务器后,服务器需要使用服务器入口。射流研究…提供的函数,将组件渲染成html。

第四步:HTTP服务所有东西的准备好之后,我们需要修改nodejs的超文本传送协议服务器的启动文件。首先,加载服务端代码服务器入口。射流研究…的网络包构建结果

const path=require(' path ');\ n常量服务器捆绑包=路径。解决(流程。CWD(),' serverDist '' vue-SSR-server-bundle。JSON’);\ n const { createBundleRenderer }=require(' vue-server-renderer ');\ n常量服务器捆绑包=路径。解决(流程。CWD(),' serverDist '' vue-SSR-server-bundle。JSON’);加载客户端代码客户端条目。射流研究…的网络包构建结果

const客户端清单路径=路径。解决(流程。CWD(),' dist '' vue-SSR-client-manifest。JSON’);\ n const客户端清单=require(客户端清单路径);使用服务器渲染器的createBundleRenderer创建一个超文本标记语言渲染器:

const模板=fs。读取文件同步(路径。resolve(_ _ dirname,“index.html”),“utf-8”);\ n const renderer=createBundleRenderer(服务器捆绑,{模板,//使用超文本标记语言模板 clientManifest //将客户端的构建结果清单传入\ n });创建超文本标记语言模板,index.html

html \ n head \ n titles SSR/title \ n/head \ n body \ n!- vue-ssr-outlet – /body/html在超文本标记语言模板中,通过传入的客户端渲染结果客户端清单,将自动注入所有环样式表标签,而占位符将会被替换成模板组件被渲染后的具体的超文本标记语言片段和脚本脚本标签。

HTML准备好之后,我们暂停服务器中的所有路由请求。

const express=require(' express ');\ n const app=express();/* code todo实例化renderer renderer */\ n \ napp . get(' * 'function (req,RES){ \ nrenderer . rendertostring({ },(err,html)={\ n if (err) {\ n res.send,\ n return\ n } \ n RES . send(html);\ n })\ n });接下来,我们构建客户端和服务器项目,然后执行node server.js并打开页面源代码。

e5b8a72005ba42ac97494a572a13bd6b?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=E6%2BMD52%2FlPb25vhoWqidmhkOIZo%3D&index=10

看似符合预期,却发现控制台出现错误,客户端build css和js无法加载。原因很清楚。我们没有将客户端构建结果文件挂载到服务器的静态资源目录中。在装载路由之前,添加以下代码:

app . use(express . static(path . resolve(process . CWD(),' dist '));看起来你完了。点击say hello也会弹出一条信息。细心的同学会发现,根节点有一个数据服务器渲染的属性。这个属性有什么作用?

既然服务器已经渲染了HTML,我们显然不需要丢弃它,重新创建所有的DOM元素。相反,我们需要“激活”这些静态HTML,然后使它们成为动态的(能够响应随后的数据变化)。

如果您检查服务器呈现的输出结果,一个特殊的属性会添加到应用程序的根元素中:

div id=' app ' data-server-rendered=' true ' data-server-rendered是一个特殊的属性,用来让客户端Vue知道HTML的这一部分是由Vue在服务器端渲染的,应该在激活模式下挂载。

路由的共享与同步模板组件共享完成后,下面就是路由的共享。我们面前的服务器使用的路由是*,接受任何URL,这样就可以让所有的URL请求都交给Vue进行路由处理,从而完成客户端路由和服务器路由的复用。

步骤1:创建路由器实例。为了实现重用,像createApp一样,我们创建了一个createRouter.js

从“Vue”导入Vue;\ n从“vue-router”导入路由器;\ n从'导入主目录。/views/Home '\ n导入关于来自。/views/About '\ n UE . use(Router)\ n const routes=[{ \ n path:'/' name: 'Home ' component: Home},{ path: '/about ' name: 'About '\ n component:About \ n }];\ n导出默认函数createrouter () {\ n返回newrouter ({\ n mode:' history '\ n routes \ n}) \ n}在createApp.js中创建路由器

从“Vue”导入Vue;\ n从'导入应用程序。/App '\ n导入从'创建路由器。/create router '\ n \ n导出默认函数create app(context){ \ n const router=create router();//创建路由器实例 const app=new Vue({ router,//将路由器注入根Vue实例\ n render:h=h(App)\ n });返回{路由器,应用};\ n };第二步:路由器做好路由匹配后,修改server-entry.js,将请求的URL传递给路由器,这样在创建app时就可以根据URL匹配对应的路由,然后就可以知道需要渲染哪些组件了。

从'导入createApp。/create app '\ n \ n导出默认上下文={ //因为它可能是异步路由挂钩函数或组件,所以我们将返回一个承诺, //以便服务器可以等待所有内容准备就绪后再进行呈现。 return new Promise((resolve,reject)={ const { app,router }=create app(); //在服务器端设置路由器的位置\ n router . push(context . URL)\ n//on ready等待,直到路由器解析了可能的异步组件和挂钩函数\ n router . on ready()={ \ n const matched components=router . get matched components(); //如果路由无法匹配,则执行reject函数并返回404 if(!matchedComponents.length) {退货拒绝({代码:404 \ n }); } //Promise应解析应用程序实例,以便它可以呈现\ n resolve (app) \ n},reject \ n}) \ n}修改server.js的路由,并将url传递给呈现器。

app.get('* 'function(req,RES){ \ n const context={ \ n URL:req . URL \ n };\ n renderer . rendertostring(context,(err,html)={ \ n if(err){ \ n console . log(err); res.send('500服务器错误');返回;\ n } \ n RES . send(html);\ n })\ n });为了测试,我们将App.vue修改为路由器视图。

template \ n div id=' app ' \ n router-link to='/' Home/router-link \ n router-link to='/about ' about/router-link \ n router-view/\ n/div \ n/template Home . vue

template \ n div home Page/div \ n/template about . vue

Template \ n div关于页面/div \ n/template编译、运行和查看源代码。

6692295f3e3046f1843ac229b8ed3da4?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=Avlkwtwet8gGOyLyavIgOgunVMQ%3D&index=11

点击路由没有刷新页面,但是客户端路由跳转了,一切都符合预期。

数据共享和状态同步之前我们只是简单的实现了服务器端的渲染,但是现实中我们在访问页面的时候,还需要获取要渲染的数据并渲染成HTML。也就是说,在渲染HTML之前,我们需要准备好所有的数据,然后传递给渲染器。

通常,在Vue中,我们将状态数据交给Vuex进行管理。当然,状态也可以存储在组件中,但是我们需要在组件实例化时自己同步数据。

步骤1:创建一个商店实例。与createApp类似,第一步是创建一个createstore.js来实例化商店,并将其提供给客户机和服务器。

从“Vue”导入Vue;\ nim从“Vuex”导入Vuex;\ n从'导入{fetchItem}。/API '\ n \ nvue . use(Vuex);\ n \ n导出默认函数createStore() {返回新的Vuex。Store({ state: { item: {} },\ n actions:{ \ n fetchItem({ commit },id) {\ n return fetchItem(id)。然后(item={ commit('setItem 'item); }) } },突变:{ setItem(state,item) { Vue.set(state.item,item);\ n } \ n } \ n })\ n }操作封装了请求数据的功能,而变化用于设置状态。

将createstore添加到createApp中,将store注入到vue实例中,这样所有的Vue组件都可以得到store实例。

导出默认函数create app(context){ \ n const router=create router();\ n const store=createStore();\ n constapp=newVue ({\ n router,\ n store,//将存储注入到根Vue实例\ n render:h=h(App)\ n }); return {路由器、商店、应用};\ n };为了测试方便,我们模拟了一个远程服务函数。

fetchItem,用于查询对应item

export function fetchItem(id) { const items = [ { name: ‘item1’, id: 1 }, { name: ‘item2’, id: 2 }, { name: ‘item3’, id: 3 } ]; const item = items.find(i =>> i.id == id); return Promise.resolve(item);}第二步:STORE连接组件

一般情况下,我们需要通过访问路由,来决定获取哪部分数据,这也决定了哪些组件需要渲染。事实上,给定路由所需的数据,也是在该路由上渲染组件时所需的数据。所以,我们需要在路由的组件中放置数据预取逻辑函数。

在Home组件中自定义一个静态函数asyncData,需要注意的是,由于此函数会在组件实例化之前调用,所以它无法访问 this。需要将 store 和路由信息作为参数传递进去

<template>><div>> <div>>id: {{item.id}}</div>> <div>>name: {{item.name}}</div>></div>></template>><script>>export default { asyncData({ store, route }) { // 触发 action 后,会返回 Promise return store.dispatch(‘fetchItems’, route.params.id) }, computed: { // 从 store 的 state 对象中的获取 item。 item() { return this.$store.state.item; } }}</script>>第三步:服务端获取数据

在服务器的入口文件server-entry.js中,我们通过URL路由匹配 router.getMatchedComponents()得到了需要渲染的组件,这个时候我们可以调用组件内部的asyncData方法,将所需要的所有数据都获取完后,传递给渲染器renderer上下文。

修改createApp,在路由组件匹配到了之后,调用asyncData方法,获取数据后传递给renderer

import createApp from ‘./createApp’;export default context =>> { // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, // 以便服务器能够等待所有的内容在渲染前就已经准备就绪。 return new Promise((resolve, reject) =>> { const { app, router, store } = createApp(); // 设置服务器端 router 的位置 router.push(context.url) // onReady 等到 router 将可能的异步组件和钩子函数解析完 router.onReady(() =>> { const matchedComponents = router.getMatchedComponents(); // 匹配不到的路由,执行 reject 函数,并返回 404 if (!matchedComponents.length) { return reject({ code: 404 }) } // 对所有匹配的路由组件调用 `asyncData()` Promise.all(matchedComponents.map(Component =>> { if (Component.asyncData) { return Component.asyncData({ store, route: router.currentRoute }); } })).then(() =>> { // 状态传递给renderer的上下文,方便后面客户端激活数据 context.state = store.state resolve(app) }).catch(reject); }, reject); })}

将state存入context后,在服务端渲染HTML时候,也就是渲染template的时候,context.state会被序列化到window.__INITIAL_STATE__中,方便客户端激活数据。

第四步:客户端激活状态数据

服务端预请求数据之后,通过将数据注入到组件中,渲染组件并转化成HTML,然后吐给客户端,那么客户端为了激活后端返回的HTML被解析后的DOM节点,需要将后端渲染组件时用的store的state也同步到浏览器的store中,保证在页面渲染的时候保持与服务器渲染时的数据是一致的,才能完成DOM的激活,也就是我们前面说到的data-server-rendered标记。

在服务端的渲染中,state已经被序列化到了window.__INITIAL_STATE__,比如我们访问http://localhost:3001?id=1,查看页面源代码

092b4b10b304416689ef66c4386c88ae?_iz=31825&from=article.detail&x-expires=1702415116&x-signature=CG4cEPn6j%2B6EphQaieZYmtTNAA8%3D&index=12

可以看到,状态已经被序列化到window.__INITIAL_STATE__中,我们需要做的就是将这个window.__INITIAL_STATE__在客户端渲染之前,同步到客户端的store中,下面修改client-entry.js

const { app, router, store } = createApp();if (window.__INITIAL_STATE__) { // 激活状态数据 store.replaceState(window.__INITIAL_STATE__);}router.onReady(() =>> { app.$mount(‘#app’, true);});

通过使用store的replaceState函数,将window.__INITIAL_STATE__同步到store内部,完成数据模型的状态同步。

总结

当浏览器访问服务端渲染项目时,服务端将URL传给到预选构建好的VUE应用渲染器,渲染器匹配到对应的路由的组件之后,执行我们预先在组件内定义的asyncData方法获取数据,并将获取完的数据传递给渲染器的上下文,利用template组装成HTML,并将HTML和状态state一并吐给前端浏览器,浏览器加载了构建好的客户端VUE应用后,将state数据同步到前端的store中,并根据数据激活后端返回的被浏览器解析为DOM元素的HTML文本,完成了数据状态、路由、组件的同步,同时使得页面得到直出,较少了白屏时间,有了更好的加载体验,同时更有利于SEO。

个人觉得了解服务端渲染,有助于提升前端工程师的综合能力,因为它的内容除了前端框架,还有前端构建和后端内容,是一个性价比还挺高的知识,不学白不学,加油!

浏览器的渲染过程

最近朋友跟我分享了一个场景。他需要用浏览器实时生成数万个二维码,并打包压缩。现在功能实现了,但是时间长,一旦开始生成,页面就卡的很厉害。

我一听,应该是渲染、变换、压缩等大量计算阻塞了Js执行的主线程,于是开始尝试优化方案。

先重现Js主线程方案。

这个方案精神负担最低,无非就是画布渲染成blob/(或者其他)。JSZip添加blob并压缩,最后下载保存。执行的代码总结如下:

importjszipfom ' jszip ' \ nasyncfunctiondownload(){ \ n//canvasdosth \ nconstzip=newJSZip()\ nawaitnewPromise((resolve)={ \ ncanvas . to Blob((blob)={ \ nzip . file(filename,blob!)\ n resolution(blob)\ n })\ nconst content=awaitzip . generate sync(\ n { \ n type:' blob ' \ n } \ n saves(content,zip name)}实现起来非常简单。作者还再现了在没有额外压缩的情况下,当QR码的错误级别较低时,生成10,000个QR码的情况。

每次生成图片约为3-4kb(取决于携带参数的大小),生成时间约为190,257.10ms,压缩时间为13,531ms,总时间为203,788.10 ms。

经过反复试验,得出以下影响因素:

压缩级别越高,压缩速度越慢。

生成的图像量越大,生成速度越慢,压缩速度越慢。

另外,对于这种耗时的任务,必须增加一个类似onProgress的钩子,方便用户自定义进度条优化体验,同时也要防止用户误操作功亏一篑。

工作线程多线程压缩

现在主线程被阻塞了,我们自然想到Web Worker,所以我用它来压缩图片。

选择测试材料时,使用16MB的图片。经过尝试,压缩时间明显高于图片的生成时间。(较小的图像实际上是不必要的,主线程本身压缩得足够快)

员工代码总结如下:

//main . Worker . ts \ nimportjszipffrom ' jszip ' \ nconstworker:Worker=self asany \ nasyncfunctiondoZip(array buffer:array buffer){ \ nconst zip=new jszip()\ nconst blob=new blob([array buffer])\ nzip . file(filename,blob)\ nconst content=awaitzip . generateaync(\ N { \ N type:' array buffer ' \ N },\ N({percent})=。

从“worker-loader!@/workers/main.worker '

//vue 3 ts \ ncanvas . to blob((blob)={ \ nblob . array buffer()。然后((ab)={ \ nconstmessage:zipworrequesteventdata={ \ n type:' zip '\ narray buffer:ab \ n } \ n worker . postmessage(message,[ab])\ n })\ n worker . on message=(event:messageeventzipworresponseeventdata)={ \ nconstdata=event . data \ n if(data . type==' save '){ \ nconst blob=new blob([data . contentarraybuffer])\ n saves(blob,' test.zip ')。\ n } \ nelseif(data . type==' percent '){ \ n//进度条percent.value=data.percent?0}}需要特别注意的一点:postMessage推荐写对象转移。这里需要说明的是,主线程和Web Workers之间的通信不是对象引用的传递,而是序列化/反序列化的过程。当对象非常大时,序列化和反序列化都会消耗大量的计算资源,降低运行速度。对象转移是零成本地将对象引用转移到Web Workers的上下文中,不需要结构化复制。

需要注意的是,对象引用被转移后,原上下文无法访问该对象,只有Web Workers将对象再次还原到主线程上下文后,主线程才能正常访问被转移的对象。

唯一可以引用的对象是:

数组缓冲器

消息端口

可读流

可写流

转换流

图像位图

屏幕外画布

这也是我们把图像转换成ArrayBuffer对象进行传输的主要原因。否则,复制一个数据会浪费内存和计算能力。

通过这个方案,压缩部分的计算转移到Web Worker,从而避免了主JS线程的阻塞。但是适用场景没有那么多,因为批量小图的压缩打包并不是很有利可图,图片的生成也是非常耗时耗资源的。这部分还没有解决,那么接下来,我们尝试把画布图片的渲染和生成放到Web Worker中。

屏幕外画布工作进程屏幕外呈现

Web Worker的全局范围中不存在图像。

无法将绘制的画布转换为OffscreenCanvas,它将报告:无法对“htmlcanvas element”执行“transfer control to offscreen”:无法从具有呈现上下文的画布传输控制。错误。

首先,添加@types来引入智能提示:yarn add-d @ types/off screen canvas

另外,建立了js主线程和web worker之间的传输是以ImageBitmap对象的形式进行图像传输。

首先,ImageBitmap是可转移的(参见上一节工作线程多线程压缩:引用转移介绍)

同时创建ImageBitmap的API API:createImageBitmap方法存在于windows和workers中。它接受各种图像源。例如,它可以处理我们最常用的HTMLImageElement对象。

这里,我以ImageBitmap为例构建一些Web Worker的代码:

//main . work . ts \ nasyncfunctiongetCanvasBlob(bitmap:image bitmap):promise blob { \ nconst canvas=newOffscreenCanvas(bitmap . width=' 360 px 'height='auto' /

这段代码的目的是用在Web Worker中创建的offscreen canvaselement对象替换在主线程中创建的HTMLCanvasElement对象。

主线程只负责创建ImageBitmap、ArrayBuffer等Transferable_objects与Worker进行通信。

最后,Worker向主线程输出一个ArrayBuffer来组装二进制对象,并从浏览器下载。

该方案中有两个可能的不兼容性:

屏幕外画布

OffscreenCanvas,除了chormuim内核的一批浏览器(chrome.edge)支持的比较好,比如FF,Safari,ie和IE都不支持(意外的是opera居然支持)浏览器兼容。

大多数网络工作者都支持它(包括ie10),但不同浏览器支持的特性略有不同。与浏览器兼容

//是否支持web worker \ nexportconsistussupportworker=boolean(window . worker)\ n//是否支持OffscreenCanvas导出\ nconstissupport offscreencanvas=boolean(window . offscreen canvas)先删除最不兼容的offscreen canvas?

删除辅助Web Worker,只使用Js主线程。

处理大体积图片的性能更好。

兼容性差,需要降级方案。

相比单线程版本更复杂,精神负担更重,容易出现bug。

处理小图可能不如主线程。

不同的浏览器性能不同,导致用户体验难以统一。

源代码

屏幕外画布—使用Web Worker加速画布操作

网络工作者快速入门

传输大量可传输对象时Web Worker的性能问题

transferControlToOffscreen

好玩下载

蝙蝠侠拼图,蝙蝠侠拼图图纸

2024-12-7 13:52:05

小学素材

小学教育博新语文小升初语文解题百度网盘下载

2023-10-9 14:27:42

购物车
优惠劵
搜索