概述
在我看来服务端的主要痛点就是数据的存取,有各种不同的解决方法但是哪一种都感觉不够完美。
这里通过vuex来进行服务端和客户端的数据同步,主要根据是服务端渲染完成之后如果存在store,会在window中插入一个字段来表示,客户端可以通过这个字段来直接加载。
上文里配置router,vuex的配置方式类似
先声明一个vuex的工厂函数
import Vue from 'vue'
import Vuex from 'vuex'
import {itemApi} from './api' //忽略实现细节,请求数据并返回一个promise
Vue.use(Vuex)
export function createStore(){
return new Vuex.Store({
state:{items:{}}
mutations:{
addItem(state,{id,item}){
Vue.set(state.items,id,item)
}
},
actions:{
fetchItem({commit},id){
//store.dispatch需要返回一个Promise来判断数据请求情况
return itemApi(id).then(item=>{
commit('addItem',{id,item})
})
}
}
})
}
在main里面加入vuex
import Vue from 'vue'
import {createRouter} from '@/router'
import {createStore} from '@/store'
import {sync} from 'vuex-router-sync' //新增的工具包,需要npm install安装,用来同步store和router
import App from '@/App' //根组件
export default ()=>{
const router=createRouter()
const store=createStore()
sync(store,router)
const app=new Vue({
router,
store,
render:h=>h(App)
})
return {app,router,store} //返回store
}
一个需要进行数据的组件(下文中方案2时)
<template>
<div>{{ item.title }}</div>
</template>
<script>
export default {
asyncData ({ store, route }) {
// 触发 action 后,会返回 Promise ,把这个Promise返回出去,判断状态
return store.dispatch('fetchItem', route.params.id)
},
computed: {
// 从 store 的 state 对象中的获取 item。
item () {
return this.$store.state.items[this.$route.params.id]
}
}
}
</script>
其实asyncData这个名字是随意定义的,因为他调用的时机需要手动编写代码实现
在server.js也就是服务端的入口文件里更改
//服务端的入口文件 server.js
import createApp from './main'
export default ctx => {
return new Promise((resolve, reject) => {
const { url } = ctx;
const { app, router ,store} = createApp()
router.push(url)
router.onReady(() => {
const matchs = router.getMatchedComponents()
if (!matchs.length){
return reject({code:404})
}
Promise.all(matchs.map(Component=>{ //遍历所有需要改变的组件
if(Component.asyncData){ //判断这些组件有没有asyncData函数
return Component.asyncData({ //返回这个Promise通all来全部处理
store:store,
route:router.currentRoute //传入的route为现在的route
})
}
})).then(()=>{
// 在所有预取钩子(preFetch hook) resolve 后,
// 我们的 store 现在已经填充入渲染应用程序所需的状态。
// 当我们将状态附加到上下文,
// 并且 `template` 选项用于 renderer 时,
// 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
ctx.state=store.state
//成功返回app
resolve(app)
}).catch(reject)
},reject)
})
}
在服务端的数据存取很简单,客户端需要为不同的加载方式和用户体验来做不同配置。
方案1
通过组件beforeMount生命周期请求数据,这个需要每个组件的路由变化需要直接更新,也就是每次变化都需要变更url来触发组件的beforMount,因为数据在第一时间是没有请求到的,所以需要在每个组件上编写loading并把实际代码通过if-else的方式来不挂载,等待数据完成之后再挂载,优点是页面直接变化,用户可以在第一时间感觉到。缺点是需要每个组件添加加载指示器,并且如果不做判断的话,在首屏的时候服务端已经做好的数据,但是在beforeMount时又会进行一个数据的加载,为了解决这个可能需要为数据固定一个名称。
//客户端入口文件 client.js
import Vue from 'vue'
import createApp from './main'
const {app,router,store} = createApp() //增加引入store
//注入到window里的state转化成客户端可用的state
if(window.__INITIAL_STATE__){ //判断window里有没有state
store.replaceState(window.__INITIAL_STATE__) //有的话加入到客户端中
}
//方案1
Vue.mixin({
data(){ //全局mixin一个loading
return {
loading:false
}
},
beforeMount(){ //在挂载之前
const {asyncData}=this.$options
let data=null; //把数据在computed的名称固定为data,防止重复渲染
try{
data=this.data; //通过try/catch包裹取值,防止data为空报错
}catch(e){}
if(asyncData&&!data){ //如果拥有asyncData和data为空的时候,进行数据加载
//触发loading加载为true,显示加载器不显示实际内容
this.loading=true;
//为当前组件的dataPromise赋值为这个返回的promise,通过判断这个的运行情况来改变loading状态或者进行数据的处理 (在组件内通过this.dataPromise.then保证数据存在)
this.dataPromise=asyncData({store,route:router.currentRoute})
this.dataPromise.then(()=>{
this.loading=false;
})catch(e=>{
this.loading=false;
})
}else if(asyncData){
//如果存在asyncData但是已经有数据了,也就是首屏情况的话返回一个成功函数,防止组件内因为判断then来做的操作因为没有promise报错
this.dataPromise=Promise.resolve();
}
},
)
router.onReady(()=>{
app.$mount('#app')
})
在方案1的情况下组件应该是这样的
<template>
<加载器 v-if="loading"></加载器>
<实际组件 v-else>
... ...
</实际组件>
</template>
<script>
export default={
asyncData({store,route}){
const {xx,xxx}=route.params;
return store.dispatch('xxx',{xx,xxx})
},
computed:{
data(){
return this.$store.state.xx;
}
},
beforeMount(){
this.dataPromise.then(()=>{
//对数据再处理
//computed是在被调用时才会加载数据,data在初始化时不能直接调用computed的数据否则会抛出异常,可以把赋值操作放到这里
})
}
}
</script>
方案2
在路由发生变化的时候进行调用asyncData数据加载,等待数据加载完成之后再跳转到组件上,这时候数据已经完成,缺点是因为等待数据加载然后触发跳转所以过程看起来感觉更慢,需要触发全局的加载指示器。另外在服务器渲染的时候因为首屏数据传入,所以不会有问题,但是在使用纯客户端进行开发的时候,会因为首屏不触发路由中间件,所以不执行数据加载抛出错误,经过一次跳转之后才会正常。
// ...忽略无关代码
//方案2
Vue.mixin({
//因为在不变更组件的情况下变更路由 例如 /a/1 /a/2这种情况
//beforeResolve不会触发,所以需要在这种变更的时候也读取数据
beforeRouteUpdate (to, from, next) {
const { asyncData } = this.$options
if(asyncData){
// 这里如果有加载指示器(loading indicator),就触发
asyncData({
store: this.$store,
route: to
}).then(()=>{
// 停止加载指示器(loading indicator)
next()
}).catch((e)=>{
// 停止加载指示器(loading indicator)
next();
})
}else{
next()
}
}
})
router.onReady(() => {
// 添加路由钩子函数,用于处理 asyncData.
// 在初始路由 resolve 后执行,
// 以便我们不会二次预取(double-fetch)已有的数据。
// 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve解析完毕。
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to)
const prevMatched = router.getMatchedComponents(from)
// 我们只关心非预渲染的组件
// 所以我们对比它们,找出两个匹配列表的差异组件
let diffed = false
const activated = matched.filter((c, i) => {
return diffed || (diffed = (prevMatched[i] !== c))
})
if (!activated.length) {
return next()
}
// 这里如果有加载指示器(loading indicator),就触发
Promise.all(activated.map(c => {
if (c.asyncData) {
return c.asyncData({ store, route: to })
}
})).then(() => {
// 停止加载指示器(loading indicator)
next()
}).catch(next)
})
app.$mount('#app')
})
具体方案根据项目不同可以灵活组合,这两个方案仅当参考。
需要注意的点
1. actions需要返回一个promise
2. asyncData需要返回一个promise
3. 页面的展示时期是否可以得到数据
最后
以上就是幸福心情为你收集整理的关于vue服务端渲染 2 数据预存取的全部内容,希望文章能够帮你解决关于vue服务端渲染 2 数据预存取所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复