step-06 异步数据一

我们知道vue是响应式的,渲染页面的同时异步获取数据,再将的数据渲染到界面。在服务器中我们只需要服务器返回html就行,不需要响应式数据,并且响应式也会给服务器增加负载。
所以渲染html的时候需要已经获取到数据,需要对数据进行预读取。
在访问vue网站的时候,切换页面都会通过路由,路由也决定了需要访问和渲染哪些组件,这时候就需要获取到这些组件中的数据,所以数据的预读取我们就在路由中进行。
另一个就是在客户端中,在打包的文件client.bundle.js挂载(vueApp.$mount(’#app’) )到页面之前,也需要获得跟服务端一样的的数据。

我们需要一个专门的容器来存储获取到的数据,客户端和服务端都使用这个容器。
这里采用vuex来存取数据,当然你也可以自己设计一个。
src目录先创建一个store.js

import Vue from 'vue'
import Vuex from 'vue'
//fetch-data.js封装了一个获取数据的api,返回promise
import { fetchData } from './fetch-data.js'

Vue.use(Vuex)

export function createStore() {
    return new Vuex.Store({
        state: {
            item: {
            }
        },
        mutations: {
            setItem(state, {id, title}) {
                Vue.set(state.item, 'id', id)
                Vue.set(state.item, 'title', title)
            }
        },
        actions: {
            fetchItem(context, url) {
                return fetchData(url).then(res => {
                    context.commit('setItem', res.item)
                })
            }
        }
    })
}

也是工厂函数的方式创建。简单说一下vuex,state是用来存放数据的,mutations是用来修改数据的,通过store.commit(‘setItem’)来触发,但是不支持异步操作。
actions是用来提交mutations的,通过store.dispatch(‘fetchItem’)来触发,获取到数据后再提交mutations中的方法修改数据,主要用于异步修改数据。

//fetch-data.js

import axios from 'axios'

export function createStore(url) {
    return new Promise((resolve, reject) => {
        axios.get(url).then(res => {
            if (res.status === 200) {
                resolve(res)
            }
        }).catch(err => {
            reject(err)
        })
    })
}

在根目录创建backend.js文件用来启动一个后台服务,数据都从这里获取。

const Koa = require('koa')
const KoaRouter = require('koa-router')
const cors = require('koa2-cors')

const app = new Koa()
const router = new KoaRouter()
// 解决跨域问题
app.use(cors({
    exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
    maxAge: 5,
    credentials: true,
    allowMethods: ['GET', 'POST'],
    allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}));

let count = 0
// 具体的数据就不从其他地方获取了,直接返回数据
const fooDate = async ctx => {
    const id = ctx.query.id || ''
    // 获取一次后自增1,让每次获取到的数据都不一样
    count++
    ctx.body = {
        title: '我是foo'+ id +'的标题' + count,
        content: '我是foo'+ id +'的内容'+ count
    }
}
const barDate = async ctx => {
    ctx.body = {
        title: '你是不是bar的标题',
        content: '你是不是bar的内容'
    }
}

app.use(async (ctx, next) => {
    //稍作区分以/fetch开头为数据请求
    if (!/^\/fetch/.test(ctx.path)) {
        ctx.body = {
            errCode: 404
        }
    } else {
        await next()
    }
})

router.get('/fetch/foo', fooDate)
router.get('/fetch/bar', barDate)
app.use(router.routes())

app.listen(3017, () => {
    console.log('server started at port:3017')
})

在main.js中加上store

import Vue from 'vue'
import app from './App.vue'
import  { createRouter } from './router.js'
import  { createStore } from './store.js'

export function createApp() {
    const router = createRouter()
    const store = createStore()
    const vueApp = new Vue({
        router,
        store,
        render: h => h(app)
    })
    return { vueApp, router, store }
}

修改entry-server.js

import { createApp } from './src/main.js'

export default context => {
    return new Promise((resolve, reject) => {
        const { vueApp, router, store } = createApp()
        router.push(context.url)
        router.onReady(() => {
            const matchedComponents = router.getMatchedComponents()
            if (!matchedComponents.length) {
                reject({
                    code: 404
                })
            } else {
                Promise.all(matchedComponents.map(component => {
                    // 如果组件里有asyncData方法,就返回该方法,需要是Promise
                    // store.dispatch()返回的就是Promise
                    if (component.asyncData) {
                        return component.asyncData({ store })
                    }
                })).then(() => {
                    // store.dispatch()执行后,数据已经存到store里面
                    context.state = store.state
                    resolve(vueApp)
                }).catch(reject)
            }
        }, reject)
    })
}

我们看到组件中的asyncData函数在路由onReady中调用的,此时还没有组件还没有实例化,所以是访问不到this的,什么this. s t o r e 、 t h i s . store、this. storethis.router都访问不到,
需要在参数中传入store等。
说下这句:context.state = store.state
在将数据存到store后,将数据附加到上下文(context)中,这样在renderer选项中有template时候会将数据存到window.__INITIAL_STATE__中,并注入到html

    const renderer = vueRenderer.createBundleRenderer(bundle, {
        template: template  //将数据附加到上下文(context)中后有template选项会在window.__INITIAL_STATE__中加入数据
    })

此时服务端返回的html代码中有
客户端可以通过store.replaceState方法修改state的值。
修改Foo和Bar组件

<template>
    <div>
        <p>这是Bar页面</p>
        <p>标题:{{item.title}}</p>
        <p>内容:{{item.content}}</p>
    </div>
</template>

<script>
export default {
    asyncData({ store }) {
        return store.dispatch('fetchItem', 'http://localhost:3017/fetch/bar')
    },
    data() {
        return {
        }
    },
    computed: {
        //data中没有item,所以在computed里加上,并从store.state中获取数据
        item() {
            return this.$store.state.item
        }
    }
}
</script>
<style  scoped>
</style>

客户端数据预取有2种方式:
第一种是触发路由时候就进行数据预取,等到数据全部获取到再进行渲染,好处是一次就将页面渲染出来,坏处是获取到数据之前一直没有内容呈现,数据获取慢的话一般要加上数据加载指示器

//client.entry.js

import { createApp } from './src/main.js'

const { vueApp, router, store } = createApp()
// 服务端会在html里加入window.__INITIAL_STATE__
if (window.__INITIAL_STATE__) {
    store.replaceState(window.__INITIAL_STATE__)
}
router.onReady(() => {
    //这里要使用到达路由组件的数据,异步组件是在beforeEach之后beforeResolve之前获取的。所以在 `router.beforeResolve()`时获取异步数据,
    //以便确保所有异步组件都能获取到。
    router.beforeResolve(async (to, from, next) => {
        // 检查to和from的组件是否有相同的,不同的组件才获取数据
        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) {
            next()
        } else {
            // 有加载指示器就在放在这,比如那张经典的加载中..菊花图
            Promise.all(activated.map(c => {
                if (c.asyncData) {
                    return c.asyncData({ store })
                }
            })).then(() => {
                //数据已经加载完了,加载指示器在这里取消
                next()
            }).catch(next)
            
        }
    })
    vueApp.$mount('#app') 
}) 

注意:第一次打开页面的时候会先运行router.onReady,此时才会添加全局路由守卫router.beforeResolve,由于router.beforeResolve的执行顺序在router.onReady之前,所以第一次打开页面的时候并没有运行组件中的asyncData函数,entry-client.js中还需要加入代码

if (window.__INITIAL_STATE__) {
    store.replaceState(window.__INITIAL_STATE__)
}

那么可不可以router.beforeResolve写在router.onReady()外面呢?

router.beforeResolve(async (to, from, next) => {
    //其他代码asyncData()等
})
router.onReady(() => {
    vueApp.$mount('#app') 
})

这样写的话服务器会获取一次数据,浏览器端再获取一次,2次获取可能会导致获取到的数据不一致,如果2次获取到的数据不一样,开发模式下client.bundle.js接管页面后会用客户端获取到的数据替换掉服务端获取到的数据。

第二种方法是获取到视图后再获取数据,这种方式跟我们平时使用vue的方式有点像,点击链接触发路由后直接渲染新的组件,异步数据获取到后再渲染上去。
这种方式可以采用vue的全局混入(mixin)来实现

Vue.mixin({
    beforeMount() {
        const { asyncData } = this.$options
        if (asyncData) {
            asyncData({
                store: this.$store
            })
        }
    }
})

源码地址

上一篇: step-05热重载及路由守卫
下一篇: step-07 异步数据二

你可能感兴趣的:(javascript,vue,vue,javascript)