当前位置: 首页 > 后端技术 > Node.js

服务器渲染---数据预取和状态

时间:2023-04-03 18:16:12 Node.js

webpack从头开始构建webpack4从头开始构建(一)webpack4+React16项目搭建(二)webpack4功能配置划分细化(三)webpack4引入AntDesign和Typescript(四)webpack4代码去重、简化信息和构建优化(五)webpack4配置Vue版本脚手架(六)服务端渲染系列服务端渲染---Vue+Koa从无到有构建成功输出页面服务端渲染---数据预取和状态数据Prefetchingandstate在服务器端渲染(SSR)期间,如果应用程序依赖于一些异步数据,则需要在开始渲染过程之前对其进行预取和解析。在安装到客户端应用程序之前,需要获取与服务器端应用程序完全相同的数据——否则,客户端应用程序将使用与服务器端应用程序不同的状态,然后混合将失败。为了解决这个问题,获取的数据需要位于视图组件之外,即在专用数据存储或“状态容器”中。服务器端数据预取vuex/index.js我们引入了Vuex状态管理器来同步数据。如何获取异步数据可以根据个人需要使用。只要兼容客户端和服务端,我们先用定时器来模拟请求。//store.jsimportVuefrom'vue'importVuexfrom'vuex'Vue.use(Vuex)exportfunctioncreateStore(){returnnewVuex.Store({state:{items:{}},actions:{fetchItem({commit},id){//假设我们有一个//返回Promises的通用API(请忽略API实现细节)//`store.dispatch()`将返回Promises,//这样我们就知道数据在何时更新returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve({name:123})},500)}).then((item)=>{commit('setItem',{id,item})})}},mutations:{setItem(state,{id,item}){Vue.set(state.items,id,item)}}})}src/app.js引入vuex配置该文件,同时使用vuex-router-sync同步路由状态,从而从store获取部分路由信息//app.jsimportVuefrom'vue'//同步vue-router的当前$route作为vuexstore状态的一部分.import{sync}from'vuex-router-sync'importAppfrom'./App.vue'importcreateRouterfrom'./router'importcreateStorefrom'./vuex'exportdefaultfunctioncreateApp(){//创建路由器实例constrouter=createRouter()conststore=createStore()//将路由状态(routestate)同步到storesync(store,router)constapp=newVue({//将router注入根Vue实例router,store,render:(h)=>h(App)})//returnappandrouterreturn{app,router,store}}page/view1.vue因为服务端渲染也叫首屏渲染,也就是只把当前页面发送给客户端,同理只有到了获取页面需要的数据的时候,所以我们把请求的来源放在路由组件层面。组件暴露了一个自定义的静态函数asyncData,因为这个函数会在组件实例化之前被调用,所以不能访问this。但是我们可以传入route和store作为参数来获取需要的信息。page/view2.vue可以得到entry/entry-server.js服务器获取匹配路由的逻辑在这里,所以我们也是,获取到之后调用静态函数获取匹配的路由。因为这是一个时不时的异步操作,所以你需要使用Promise.all来确保所有匹配的组件都被成功调用,然后才能进行下一步。记得添加捕获错误操作。这样可以保证数据预取完成后,填充到store中进行渲染importcreateAppfrom'../src/app'exportdefault(context)=>{//因为可能是异步路由钩子函数或组件,所以我们将返回一个Promise,//这样服务器就可以在渲染之前等待一切准备就绪,//。returnnewPromise((resolve,reject)=>{const{app,router,store}=createApp()//设置服务器端路由器的位置router.push(context.url)//等到路由器解析可能的异步组件和钩子函数router.onReady(()=>{constmatchedComponents=router.getMatchedComponents()//匹配到路由没有找到,执行reject函数,返回404if(!matchedComponents.length){returnreject({code:404})}//为所有匹配的路由组件调用`asyncData()`Promise.all(matchedComponents.map((Component)=>{if(Component.asyncData){returnComponent.asyncData({store,route:router.currentRoute})}})).then(()=>{//在所有preFetch挂钩解析之后,//我们的商店现在填充到呈现应用程序中Desiredstate//当我们附加状态到上下文,//并且`template`选项用于渲染器,//状态将自动序列化为`window.__INITIAL_STATE__`并注入到HTML中。context.state=store.state//Promise应该解析应用程序实例,以便它可以呈现resolve(app)}).catch(reject)},reject)})}createBundleRenderer会自动将附加的上下文数据序列化到window.__INITIAL_STATE__并注入HTMLentry/entry-client.js服务器渲染已经将store序列化赋值给了页面的window.__INITIAL_STATE__字段,然后我们就可以在客户端渲染之前获取到,然后调用replaceState直接覆盖客户端的store,达到前后端共享的目的state.importcreateAppfrom'../src/app'const{app,router,store}=createApp()//挂载数据if(window.__INITIAL_STATE__){//替换store的根state,只使用state合并或时间旅行调试。store.replaceState(window.__INITIAL_STATE__)}//调用router.onReady(()=>{//挂载App.vue模板中的根元素app.$mount('#app')})router/index.js同步更改路由配置以获取idimportVuefrom'vue'importRouterfrom'vue-router'Vue.use(Router)exportdefaultfunctioncreateRouter(){returnnewRouter({//记得添加mode属性,因为#以下内容不会发送到服务器,服务器不知道请求的是哪个路由:()=>import('../page/view1.vue')},{路径:'/view2:id',组件:()=>import('../page/view2.vue')}]})}src/App.vue带参数的跳转路由构建文件运行命令yarnbuildyarn开始直接访问地址http://localhost:3005/view1:1,可以看到现在界面上已经获取到数据了,但是还是有问题。如果我们点击view2,服务端已经完成了获取,所以接下来我们要解决客户端数据获取的问题。客户端数据预取(ClientDataFetching)有两种方法。导航前先解析数据,等待所有数据解析完毕再传入数据渲染视图。缺点是如果流程耗时长用户体验不好,正常操作就是给个loading图来缓和用??户情绪。所以我们可以:检查匹配的组件过滤差异路由组件globalroutinghookexecuteasyncDatafunctionmountcomponentimportcreateAppfrom'../src/app'const{app,router,store}=createApp()//mountdataif(window.__INITIAL_STATE__){//替换store的根状态,只使用状态合并或者时间旅行调试store.replaceState(window.__INITIAL_STATE__)}//路由调用router.onReady(()=>{//添加路由初始导航完成时处理asyncData的钩子函数。//在初始路由解析后执行,//这样我们就不会预取两次(双重获取)现有数据。//使用`router.beforeResolve()`来保证所有的异步组件都resolve.router.beforeResolve((to,from,next)=>{//返回目标位置或者当前路由匹配的组件Array(是数组的定义/构造类,不是一个instance).通常在服务端渲染数据预加载时使用。constmatched=router.getMatchedComponents(to)constprevMatched=router.getMatchedComponents(from)//我们只关心非预渲染组件//所以我们比较它们以找出两个匹配列表组件之间的差异letdiffed=falseconstactivated=matched.filter((component,index)=>{returndiffed||(diffed=prevMatched[index]!==component)})if(!activated.length){returnnext()}//这里如果有loadingindicator(加载指示器),触发Promise.all(activated.map((component)=>{if(component.asyncData){returncomponent.asyncData({store,route:to})}})).then(()=>{//停止加载指示器(loadingindicator)next()}).catch(next)})//在App.vue模板中挂载根元素app.$mount('#app')})reload运行代码可以发现运行正常,因为前面设置了定时器500毫秒,所以会有明显的卡顿感。最终代码可以查看仓库Vue-ssr-demo/demo2匹配视图然后获取数据和预取客户端数据逻辑放在视图组件的beforeMount函数中。当触发路由导航时,可以立即切换视图,因此应用程序响应更快。但是,传入视图在呈现时不会具有可用的完整数据。所以对于每个使用这种策略的视图组件,它都需要有一个有条件的加载状态。说白了就是需要和普通调用一样有一个默认的状态渲染视图。我们可以在获取数据后使用beforeMount生命周期重新渲染界面。这个生命周期完成了data和el数据的初始化,编译好的模板等,但是还没有挂载。BOM节点虽然不能直接访问组件当前实例,但是可以通过this.$optionspage/view2.vue重新运行代码发现运行正常,页面可以快速响应,然后切换数据会有一点延迟最终代码可以在仓库vue-ssr-demo/demo3查看