这个系列终于要结束啦。😂 上篇中,我们留了最后一个问题: 需要获取数据的 页面不是首屏 的时候,如何处理数据的请求。官方文档这一节叫 客户端数据预取(Client Data Fetching), 然而这一节的内容实际上并不像上一节 客户端数据预取 那样复杂。

面临的问题

重新审视一下我们的问题:我们要为这个页面添加前端的数据获取逻辑,这个逻辑在页面作为首屏的时候不能起作用,最好能复用后台获取数据的代码

Vue SSR 的解决方案

和 client-side only 的项目 vue-router 数据获取类似,Vue SSR 官方也提供了2种方案,而且尤大已经阐述的很清楚了,建议大家直接跳过去深入学习。这里为了这个系列的完整性,我贴出其中在路由导航之前解析数据这种场景的代码,并稍作解释:

  // client-bundle.js

  import { createApp } from './app'

  const { app, router, store } = createApp()

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

  router.onReady(() => {
      // 这里假定 App.vue 模板中根元素具有 id="app"
      app.$mount('#app')

  +   // 添加路由钩子函数,用于处理 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) {
  +        alert('fetch data in client side')
  +        return c.asyncData({ store, route: to })
  +      }
  +    })).then(() => {
  +      // 停止加载指示器(loading indicator)
  +      next()
  +    }).catch(next)
  +  })
  })
  1. 这个方案和后台的方案基本类似,思路就是在导航之前去调用所有尚未加载组件的 asyncData 方法,待所有数据都请求完成后,再跳到页面。③

  2. 代码②这段,对比现有路由的组件 (preMatched) 和目标路由的组件(matched),从中找出不同的组件, 以避免不必要的数据请求。比如下图的场景, User 组件的 asyncData 就不会调用了。

     /user/foo/profile                     /user/foo/posts
     +------------------+                  +-----------------+
     | User             |                  | User            |
     | +--------------+ |                  | +-------------+ |
     | | Profile      | |  +------------>  | | Posts       | |
     | |              | |                  | |             | |
     | +--------------+ |                  | +-------------+ |
     +------------------+                  +-----------------+
    
  3. 代码①这个位置的 router.beforeResolve 回调是 Vue router 专门为 SSR 做的新 路由守卫函数,其触发时机是在所有组件内守卫和异步路由组件被解析之后,以及导航被确认之前。在这个钩子中我们可以充分的访问各个异步组件。前台路由导航放在这个回调里,确保了上提到的 1, 2 两点可以进行。

  4. 最后,最值得注意的地方是 router.beforeResolve 这个钩子注册是在 router.ready 之后的。也就是说是在首屏组件全部确认之后,所有对于首屏来说 router.beforeResolve 里的回调并不会触发。

以上几点,保证了前文提出的所有3个问题的解决: 前端的数据获取逻辑作为首屏的时候不能起作用复用后台获取数据的代码.

看一眼我们的成果:

完整的代码请参看 commit.

总结

到此为止,我们完成了一个非常基本的 Vue SSR 框架的搭建。当然 Vue SSR 还有更多高级的内容,比如 hot reload, sourcemap, cache 等,如果大家需要在项目中用的话,则需要进一步阅读官方指南并深入研究,或者直接使用目前已经比较成熟的 Vue SSR 框架 Nuxt.js. 正如尤大说的(官方指南中的)解决方案不是限定的 - 我们发现它们对我们来说很好,但这并不意味着无法继续改进, 这些技术方案都不是死的,也不一定是永远最好的,如果你有自己的需求,并且你能理解 SSR 最基本的问题和解决思路,那么你完全可以制定适合自己需求的,其他更好的方案。

系列文章: