当前位置: 首页 > Web前端 > vue.js

探讨ToC营销页面服务端渲染的必要性和背后的原理

时间:2023-04-01 11:54:51 vue.js

最近无论是在公司还是在自己的研究项目中,都在探索H5页面的服务端渲染,所以本文讨论一下service端渲染的必要性和背后的原理。我们先来看几个问题。为什么ToC的H5适合做SSR?ToC类营销H5页面的典型特征是:流量大,交互相对简单(尤其是搭建平台搭建的活动页面),一般页面首屏的知名度都比较高。那么为什么此时作为传统的CSR渲染方式就不合适了呢?看完下一节,或许你就有了答案。为什么服务器端渲染比客户端渲染快?下面分别对比一下两者的DOM渲染过程。图片来源ServerSideRenderingoverClientSideRendering的好处ClientRenderingServerRenderingClientRendering,需要先得到一个空的HTML页面(此时页面已经进入白屏),然后需要经过:request并解析JavaScript和CSS请求后端服务器获取数据,并根据数据在几个过程中渲染页面,然后才能看到最终页面。尤其是在复杂的应用中,由于需要加载JavaScript脚本,应用越复杂,需要加载的JavaScript脚本越多越大,会导致应用首屏加载时间非常长,这将影响用户体验。与客户端渲染相比,服务器端渲染在用户发送页面url请求后,应用服务器返回的html字符串是一个完整的计算,可以交给浏览器直接渲染,这样渲染DOM不再受静态资源和ajax限制的影响。什么是服务器端渲染限制?但是服务器端渲染真的那么好吗?实际上,不。为了实现服务器端渲染,应用程序代码需要同时兼容服务器端和客户端操作。对第三方库的要求比较高。Node渲染时如果想直接调用第三方库,该库必须支持服务端。使成为。相应的代码复杂度增加了很多。由于服务器对渲染HTML的需求增加,原本只需要输出静态资源文件的nodejs服务增加了渲染HTML的数据获取IO和CPU占用。如果流量急剧增加,可能会导致服务器崩溃,因此需要使用适当的缓存策略,并为适当的服务器负载做好准备。对构建和部署也有更高的要求。之前的SPA应用可以直接部署在静态文件服务器上,而服务端渲染应用需要在Node.js服务端运行环境。说了这么多VueSSR的原理,大家可能还不是很清楚服务端渲染的原理。下面以Vue服务端渲染为例简单介绍一下它的原理:这张图来自VueSSR指导原理分析参考Howtobuilda高可用服务端渲染项目的源码是我们的源码区,那个是,项目代码。UniversalApplicationCode和我们通常的客户端渲染代码组织完全一样,因为渲染过程是在Node端,所以没有DOM和BOM对象,所以在beforeCreate和created中不要做涉及DOM和BOM的操作生命周期钩子。app.js、Serverentry、Cliententry比client渲染的主要功能是:app.js分别向Serverentry和Cliententry暴露createApp()方法,这样每次请求都会生成一个新的app实例和Server入口和Client入口会被webpack分别打包成vue-ssr-server-bundle.json和vue-ssr-client-manifest.json。Node会根据webpack打包好的vue-ssr-server-bundle.json调用createBundleRenderer生成一个renderer实例,然后调用renderer.renderToString生成一个完整的html字符串。Node端将渲染后的HTML字符串返回给Browser,同时Node端基于vue-ssr-client-manifest.json生成的js会和HTML字符串进行hydrate,完成HTML在on上的激活客户端,使页面具有交互性。写个demo实现SSR。我们知道目前市面上实现服务端渲染一般有以下几种方式:使用next的服务端渲染方案。也就是上面提到的)使用node+ReactrenderToStaticMarkup/renderToString实现react项目的服务端渲染使用模板引擎实现ssr(如ejs、jade、pug等)最近要改造的项目刚好是开发的Vue,目前正在考虑基于vue-server-renderer改造为服务端渲染。基于以上分析的原理,我从零开始一步步搭建了一个最小的vue-ssr。有需要的可以直接使用~这里贴几个注意事项:使用SSR时没有单例模式,我们知道Node.js服务器是一个长时间运行的进程。当我们的代码进入进程时,它会做一次fetch,并保存在内存中。这意味着如果创建了单例对象,它将在每个传入请求之间共享。所以每次用户请求都会创建一个新的Vue实例,这也是为了避免跨请求状态污染的发生。因此,与其直接创建应用实例,不如暴露一个可以重复执行的工厂函数,为每个请求创建一个新的应用实例://main.jsimportVuefrom"vue";importAppfrom"./App.vue”;从“./router”导入createRouter;从“./store”导入createStore;导出默认值()=>{constrouter=createRouter();conststore=createStore();constapp=newVue({router,store,render:(h)=>h(App),});返回{应用程序、路由器、商店};};服务端代码构建和客户端代码构建的区别在于:不需要编译CSS,服务端渲染会自动将内置的CSS目标设置到nodejs环境中,无需代码切割。Nodejs一次性加载所有代码到内存,更有利于运行效率//vue.config.js//两个插件分别负责封装客户端和服务端constVueSSRServerPlugin=require("vue-server-渲染器/服务器插件");constVueSSRClientPlugin=require("vue-server-renderer/client-plugin");constnodeExternals=require("webpack-node-externals");constmerge=require("lodash.merge");//根据传入的环境变量确定入口文件和对应的配置项constTARGET_NODE=process.env.WEBPACK_TARGET===“节点”;常量目标=TARGET_NODE?"server":"client";module.exports={css:{extract:false,},outputDir:"./dist/"+target,configureWebpack:()=>({//将入口指向应用程序的服务器/clientfileentry:`./src/${target}-entry.js`,//为bundlerenderer提供源映射支持处理动态导入的方式,//也告诉`vue-loader`在编译Vue组件时输出面向服务器的代码target:TARGET_NODE?"node":"web",//是否模拟节点全局变量node:TARGET_NODE?undefined:false,output:{//使用Node样式导出模块libraryTarget:TARGET_NODE?"commonjs2":undefined,},externals:TARGET_NODE?nodeExternals({allowlist:[/\.css$/],}):undefined,optimization:{splitChunks:undefined,},//这是一个将服务器的整个输出构建到单个JSON文件中的插件。//服务器的默认文件名是`vue-ssr-server-bundle.json`//客户端默认文件名为“vue-ssr-client-manifest.json”。插件:[TARGET_NODE?newVueSSRServerPlugin():newVueSSRClientPlugin(),],}),chainWebpack:(config)=>{//cli4项目添加if(TARGET_NODE){config.optimization.delete("splitChunks");}config.module.rule("vue").use("vue-loader").tap((options)=>{merge(options,{optimizeSSR:false,});});},};处理CSS对于正常的服务器端路由,我们可以这样写:router.get("/",async(ctx)=>{ctx.body=awaitrender.renderToString();});但是这样打包之后,启动服务器,你会发现样式并没有生效。我们需要通过promise来解决这个问题:pp.use(async(ctx)=>{try{ctx.body=awaitnewPromise((resolve,reject)=>{render.renderToString({url:ctx.url},(err,data)=>{console.log("data",data);如果(err)reject(err);resolve(data);});});}catch(error){ctx.body="404";}});事件不生效的原因是我们没有激活客户端,即将客户端打包的clientBundle.js挂载到html中。首先,我们需要在App.vue的根节点添加app的id:然后通过server-invue-server-rendererplugin和client-plugin分别生成vue-ssr-server-bundle.json和vue-ssr-client-manifest.json文件,即服务端映射和客户端映射。最后在节点服务这里做一个关联:constServerBundle=require("./dist/server/vue-ssr-server-bundle.json");consttemplate=fs.readFileSync("./public/index.html","utf8");constclientManifest=require("./dist/client/vue-ssr-client-manifest.json");constrender=VueServerRender.createBundleRenderer(ServerBundle,{runInNewContext:false,//推荐模板,clientManifest,});这样就完成了客户端激活操作,支持css和events。数据模型共享和状态同步在服务器端渲染生成html之前,我们需要提前获取并解析依赖数据。同时,客户端在挂载(mounted)之前,需要获取与服务端完全一致的数据,否则客户端会因为数据不一致而混入失败。为了解决这个问题,将预取的数据存储在一个状态管理器(store)中,以保证数据的一致性。首先是为客户端和服务器创建一个商店实例://src/store.jsimportVuefrom"vue";importVuexfrom"vuex";Vue.use(Vuex);exportdefault()=>{conststore=newVuex.Store({state:{name:"",},mutations:{changeName(state){state.name="cosen";},},actions:{changeName({commit}){returnnewPromise((resolve,reject)=>{setTimeout(()=>{commit("changeName");resolve();},1000);});},},});返回商店;};将createStore添加到createApp中,并将store注入到vue实例中,这样所有Vue组件都可以获取到store实例:importVuefrom"vue";importAppfrom"./App.vue";importcreateRouterfrom"./router";+importcreateStorefrom"./store";exportdefault()=>{constrouter=createRouter();+conststore=createStore();constapp=newVue({router,+store,render:(h)=>h(App),});+return{app,router,store};};在page://src/components/Foo.vue如果你用过nuxt,你一定知道nuxt中有一个叫asyncData的钩子,我们可以在这个钩子里发起一些请求,这些请求是在服务端发送的那我们看看如何实现asyncData。在server-entry.js中,我们通过constmatches=router.getMatchedComponents()获取所有匹配当前路由的组??件,即我们可以获取所有组件的asyncData方法://src/server-entry.js//服务端渲染只需要将渲染后的实例导出importcreateAppfrom"./main";exportdefault(context)=>{const{url}=context;returnnewPromise((resolve,reject)=>{console.log("url",url);//if(url.endsWith(".js")){//resolve(app);//return;//}const{app,router,store}=createApp();router.push(url);router.onReady(()=>{constmatchComponents=router.getMatchedComponents();console.log("matchComponents",matchComponents)复制代码;if(!matchComponents.length){reject({code:404});}//resolve(app);Promise.all(matchComponents.map((component)=>{if(component.asyncData){返回组件。asyncData({store,route:router.current路线,});}})).then(()=>{//Promise.all中的方法会改变store中的状态//将vuex的状态挂载到上下文中context.state=store.state;resolve(app);}).catch(拒绝);},拒绝);});};通过Promise.all,我们可以在所有匹配到的组件中执行asyncData,然后修改服务端的store,同时也将客户端最新的store同步到客户端的store。客户端在上一步激活状态数据并将状态保存到上下文中后,在服务端渲染HTML时,也就是渲染模板时,会把context.state序列化到window.__INITIAL_STATE__中:可以看到,state已经序列化到window.__INITIAL_STATE__中了,我们要做的就是在客户端渲染之前把这个window.__INITIAL_STATE__同步到客户端的store中,修改client-entry.js如下://客户端渲染手动挂载到dom从元素上的“./main”导入createApp;const{app,router,store}=createApp();//浏览器执行时,需要将客户端的store替换为服务端最新的store状态(window.__INITIAL_STATE__){//激活状态数据store.replaceState(window.__INITIAL_STATE__);}router.onReady(()=>{app.$mount("#app",true);});通过使用store的replaceState函数,将window.__INITIAL_STATE__同步到store内部,完成数据模型的状态同步。

猜你喜欢