Skip to content

数据预取

数据预取的关键在于,在服务器端渲染过程中,提前获取并处理好组件所需的任何动态数据。这些数据可以来自 API 请求、数据库查询或其他任何形式的数据源。通过这种方式,可以在生成 HTML 之前填充好所有的内容,避免用户看到空白或加载中的页面。

创建仓库

store 目录下创建一个 index.js 文件,并定义一个名为 createStore 的函数,返回一个 Vuex Store 实例。

javascript
// src/store/index.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export function createStore() {
  return new Vuex.Store({
    state: {
      name: "",
    },
    mutations: {
      setName(state, payload) {
        state.name = payload;
      },
    },
    actions: {
      getName({ commit }) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve("SSR");
          }, 300);
        }).then((res) => {
          commit("setName", res);
        });
      },
    },
  });
}

组件中使用

在组件中,你可以通过 this.$store.state 访问到预取的数据。例如:

vue
<!-- src/components/Demo.vue -->
<template>
  <div class="demo">
    <h1>欢迎,{{ name }}</h1>
  </div>
</template>

<script>
  import { mapState } from "vuex";
  export default {
    name: "HelloWorld",
    props: {
      msg: String,
    },
    computed: {
      ...mapState(["name"]),
    },
    created() {
      this.$store.dispatch("getName");
    },
  };
</script>

main.js 文件调整

js
// src/main.js
import Vue from "vue";
import App from "./App.vue";
import createStore from "./store";

export default function () {
  const store = createStore();
  const app = new Vue({
    render: (h) => h(App),
    store, // 将 store 注入到 Vue 实例中
  });
  return { app, store };
}

组件中定义 asyncData 方法

你可以在每个需要预取数据的 Vue 组件中定义一个静态方法 asyncData。这个方法会在组件被匹配到路由时调用,并返回一个 Promise 或者直接返回对象。例如:

javascript
import { mapState } from "vuex";
export default {
  data() {
    return {};
  },
  computed: {
    ...mapState(["name"]),
  },
  asyncData({ store, route }) {
    // 这里假设有一个 Vuex action 可以获取数据
    return store.dispatch("getName");
  },
  // ...
};

服务器端

服务器端入口文件 entry-server.js

回到服务器端入口文件 (entry-server.js),我们需要确保在渲染之前完成了所有组件的数据预取。这部分代码会遍历所有匹配的组件,并调用它们的 asyncData 方法。一旦所有的数据都已经被获取,我们就继续渲染应用程序。

javascript
import createApp from "../main.js";

export default (context) => {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp();

    router.push(context.url);
    // 等待所有异步组件和钩子完成
    router.onReady(async () => {
      const matchedComponents = router.getMatchedComponents();
      if (!matchedComponents.length) {
        reject({ code: 404 });
      }
      // 对匹配的组件预取数据
      const promises = matchedComponents.map((Component) => {
        if (Component.asyncData) {
          return Component.asyncData({
            store,
            route: router.currentRoute.value,
          });
        }
      });

      // 如果有异步数据需要获取,则等待所有请求完成
      if (promises.length) {
        await Promise.all(promises);
      }
      context.state = store.state;
      resolve(app);
    });
  });
};

这段代码首先检查是否有任何匹配的组件实现了 asyncData 方法,然后使用 Promise.all() 来并发地执行所有的数据获取操作。这样可以确保所有必要的数据都在渲染前准备好。

服务端主文件 server/index.js

[参考:路由处理]

客户端 client-entry.js

为了让客户端也能够访问这些数据,在服务器端渲染完成后,你可以将这些数据作为全局变量注入到 HTML 模板中。这通常是在模板中添加一段 <script> 标签来完成的:

html
<!-- 通过“显示页面源码”操作可以看到注入的代码 -->
<script>
  window.__INITIAL_STATE__ = {
    /*  预取的数据 */
  };
</script>

这段脚本会在页面加载时被执行,将服务器端获取的数据赋值给一个全局变量 window.__INITIAL_STATE__

然后在客户端初始化 Vuex Store 时读取并合并这些初始状态:

javascript
import createApp from "../main.js";
//
const { app, store } = createApp();

app.$mount("#app");

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

优化与进阶

  • 错误处理:确保在数据预取过程中正确处理可能发生的错误,比如网络问题或者 API 返回无效数据。

  • 缓存策略:对于频繁访问且变化不大的数据,考虑在服务器端实现缓存机制,减少不必要的 API 调用。

  • 代码分割:利用 Webpack 的动态导入功能对不同的路由进行代码分割,只加载当前页面所需的模块,提升性能。

  • 条件性数据预取:并非所有页面都需要预取数据,可以根据具体情况决定是否执行 asyncData 方法,避免不必要的计算开销。

总结

通过上述步骤,你可以在 Vue SSR 环境中有效地实现数据预取。这种方法不仅有助于提高首屏加载速度,还能够为用户提供更流畅的交互体验。

[gitee demo]

Released under the MIT License.