VueJS: Tìm nạp trước dữ liệu và State

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Kho dữ liệu

Trong SSR, về cơ bản chúng ta đang tạo "ảnh chụp nhanh" của ứng dụng, vì vậy nếu ứng dụng dựa trên một số dữ liệu không đồng bộ, dữ liệu này cần phải được tìm nạp trước và được giải quyết trước khi chúng ta bắt đầu quá trình hiển thị.

Một mối quan tâm khác là trên máy khách, cùng một dữ liệu cần phải có sẵn trước khi chúng ta gắn ứng dụng phía máy khách - nếu không ứng dụng khách sẽ kết xuất bằng cách sử dụng trạng thái khác nhau và quá trình hydrat hóa sẽ thất bại.

Để giải quyết vấn đề này, dữ liệu được tìm nạp cần phải ở bên ngoài các component view, trong kho lưu trữ dữ liệu chuyên dụng hoặc "vùng chứa state". Trên máy chủ, chúng ta có thể tìm nạp trước và điền dữ liệu vào store trước khi hiển thị. Ngoài ra, chúng ta sẽ sắp xếp và căn chỉnh state trong HTML. Store phía máy khách có thể trực tiếp nhận trạng thái inline trước khi chúng ta gắn kết ứng dụng.

Chúng ta sẽ sử dụng thư viện quản lý state chính thức Vuex cho mục đích này. Hãy tạo một tệp store.js, với một số logic giả định để tìm nạp một mục dựa trên một id:

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// Assume we have a universal API that returns Promises
// and ignore the implementation details
import { fetchItem } from './api'

export function createStore () {
  return new Vuex.Store({
    state: {
      items: {}
    },
    actions: {
      fetchItem ({ commit }, id) {
        // return the Promise via `store.dispatch()` so that we know
        // when the data has been fetched
        return fetchItem(id).then(item => {
          commit('setItem', { id, item })
        })
      }
    },
    mutations: {
      setItem (state, { id, item }) {
        Vue.set(state.items, id, item)
      }
    }
  })
}

Và cập nhật app.js:

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

export function createApp () {
  // create router and store instances
  const router = createRouter()
  const store = createStore()

  // sync so that route state is available as part of the store
  sync(store, router)

  // create the app instance, injecting both the router and the store
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })

  // expose the app, the router and the store.
  return { app, router, store }
}

Logic Sắp xếp thứ tự với các thành phần

Vậy, chúng ta đặt mã gửi các hành động tìm nạp dữ liệu ở đâu?

Dữ liệu chúng ta cần tìm nạp được xác định bởi router đã truy cập - cũng xác định thành phần nào được hiển thị. Trên thực tế, dữ liệu cần thiết cho một router nhất định cũng là dữ liệu cần thiết cho các thành phần được hiển thị tại router đó. Vì vậy, sẽ là tự nhiên khi đặt logic tìm nạp dữ liệu bên trong các route component.

Chúng ta sẽ đưa ra một chức năng tĩnh tùy chỉnh asyncData các route component của chúng ta. Lưu ý vì hàm này sẽ được gọi trước khi các thành phần được khởi tạo, nó không có quyền truy cập this. Thông tin store và router cần được chuyển thành đối số:

<!-- Item.vue -->
<template>
  <div>{{ item.title }}</div>
</template>

<script>
export default {
  asyncData ({ store, route }) {
    // return the Promise from the action
    return store.dispatch('fetchItem', route.params.id)
  },

  computed: {
    // display the item from store state.
    item () {
      return this.$store.state.items[this.$route.params.id]
    }
  }
}
</script>

#Tìm nạp dữ liệu máy chủ

Trong entry-server.js chúng ta có thể nhận được các thành phần phù hợp bởi một router với router.getMatchedComponents(), và gọi asyncData nếu component yêu cầu nó. Sau đó, chúng ta cần đính kèm state đã giải quyết vào ngữ cảnh hiển thị.

// entry-server.js
import { createApp } from './app'

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

    router.push(context.url)

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }

      // call `asyncData()` on all matched route components
      Promise.all(matchedComponents.map(Component => {
        if (Component.asyncData) {
          return Component.asyncData({
            store,
            route: router.currentRoute
          })
        }
      })).then(() => {
        // After all preFetch hooks are resolved, our store is now
        // filled with the state needed to render the app.
        // When we attach the state to the context, and the `template` option
        // is used for the renderer, the state will automatically be
        // serialized and injected into the HTML as `window.__INITIAL_STATE__`.
        context.state = store.state

        resolve(app)
      }).catch(reject)
    }, reject)
  })
}

Khi sử dụng templatecontext.state sẽ tự động được nhúng vào HTML cuối cùng là trạng thái window.__INITIAL_STATE__. Trên máy khách, store sẽ nhận trạng thái trước khi gắn ứng dụng:

// entry-client.js

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

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

#Tìm nạp dữ liệu client

Trên client, có hai cách tiếp cận khác nhau để xử lý việc tìm nạp dữ liệu:

  1. Giải quyết dữ liệu trước khi điều hướng router:

Với chiến lược này, ứng dụng sẽ ở chế độ xem hiện tại cho đến khi dữ liệu cần thiết theo chế độ xem incoming đã được giải quyết. Lợi ích là chế độ xem incoming có thể trực tiếp hiển thị toàn bộ nội dung khi nó sẵn sàng, nhưng nếu việc lấy dữ liệu mất một thời gian dài, người dùng sẽ cảm thấy "bị kẹt" trên chế độ xem hiện tại. Do đó, chúng tôi khuyên bạn nên cung cấp chỉ báo tải dữ liệu nếu sử dụng chiến lược này.

Chúng ta có thể thực hiện chiến lược này trên máy khách bằng cách kiểm tra các thành phần phù hợp và gọi hàm asyncData của chúng bên trong một hook router toàn cục. Lưu ý chúng ta nên đăng ký hook này sau khi router ban đầu đã sẵn sàng để chúng ta không cần tìm nạp lại dữ liệu do máy chủ tìm nạp một cách không cần thiết.

// entry-client.js

// ...omitting unrelated code

router.onReady(() => {
  // Add router hook for handling asyncData.
  // Doing it after initial route is resolved so that we don't double-fetch
  // the data that we already have. Using `router.beforeResolve()` so that all
  // async components are resolved.
  router.beforeResolve((to, from, next) => {
    const matched = router.getMatchedComponents(to)
    const prevMatched = router.getMatchedComponents(from)

    // we only care about non-previously-rendered components,
    // so we compare them until the two matched lists differ
    let diffed = false
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = (prevMatched[i] !== c))
    })

    if (!activated.length) {
      return next()
    }

    // this is where we should trigger a loading indicator if there is one

    Promise.all(activated.map(c => {
      if (c.asyncData) {
        return c.asyncData({ store, route: to })
      }
    })).then(() => {

      // stop loading indicator

      next()
    }).catch(next)
  })

  app.$mount('#app')
})
  1. Tìm nạp dữ liệu sau khi chế độ xem được đối sánh được hiển thị:

Chiến lược này đặt logic tìm nạp dữ liệu phía máy khách trong hàm beforeMount của view component. Điều này cho phép các chế độ xem chuyển ngay lập tức khi điều hướng tuyến đường được kích hoạt, do đó ứng dụng cảm thấy phản hồi nhanh hơn một chút. Tuy nhiên, chế độ xem incoming sẽ không có sẵn dữ liệu đầy đủ khi được hiển thị. Do đó, cần phải có trạng thái tải có điều kiện cho mỗi view component sử dụng chiến lược này.

Điều này có thể đạt được với một mixin toàn cầu chỉ dành cho khách hàng:

Vue.mixin({
  beforeMount () {
    const { asyncData } = this.$options
    if (asyncData) {
      // assign the fetch operation to a promise
      // so that in components we can do `this.dataPromise.then(...)` to
      // perform other tasks after data is ready
      this.dataPromise = asyncData({
        store: this.$store,
        route: this.$route
      })
    }
  }
})

Hai chiến lược cuối cùng là các quyết định UX khác nhau và nên được chọn dựa trên kịch bản thực tế của ứng dụng bạn đang xây dựng. Nhưng bất kể bạn chọn chiến lược nào, hàm asyncData cũng nên được gọi khi một route component được tái sử dụng (cùng một router, nhưng các tham số hoặc truy vấn đã thay đổi. Ví dụ: từ user/1 đến user/2). Chúng tôi cũng có thể xử lý điều này với một mixin toàn cầu chỉ dành cho client:

Vue.mixin({
  beforeRouteUpdate (to, from, next) {
    const { asyncData } = this.$options
    if (asyncData) {
      asyncData({
        store: this.$store,
        route: to
      }).then(next).catch(next)
    } else {
      next()
    }
  }
})

Tách mã store

Trong một ứng dụng lớn, store Vuex của chúng ta có thể sẽ được chia thành nhiều mô-đun. Tất nhiên, cũng có thể chia mã các mô-đun này thành các đoạn route component tương ứng. Giả sử chúng ta có mô-đun store sau:

// store/modules/foo.js
export default {
  namespaced: true,
  // IMPORTANT: state phải là một hàm để module có thể
  // được thể hiện nhiều lần
  state: () => ({
    count: 0
  }),
  actions: {
    inc: ({ commit }) => commit('inc')
  },
  mutations: {
    inc: state => state.count++
  }
}

Chúng ta có thể sử dụng store.registerModule để lazy-register module này trong hook asyncData của component route:

// inside a route component
<template>
  <div>{{ fooCount }}</div>
</template>

<script>
// import module ở đây thay vì đặt trong `store/index.js`
import fooStoreModule from '../store/modules/foo'

export default {
  asyncData ({ store }) {
    store.registerModule('foo', fooStoreModule)
    return store.dispatch('foo/inc')
  },

  // IMPORTANT: tránh trùng lặp module đăng ký trên client
  // khi route được thăm nhiều lần.
  destroyed () {
    this.$store.unregisterModule('foo')
  },

  computed: {
    fooCount () {
      return this.$store.state.foo.count
    }
  }
}
</script>

Vì mô-đun hiện là dependency của route component, nên nó sẽ được chuyển vào đoạn không đồng bộ của route component bằng gói webpack.


Như vậy là có rất nhiều code! Điều này là do tìm nạp dữ liệu phổ biến có lẽ là vấn đề phức tạp nhất trong ứng dụng được máy chủ trả về và chúng ta đang đặt nền tảng để phát triển hơn nữa và dễ dàng hơn nữa. Khi bản mẫu được thiết lập, việc tạo ra các component riêng lẻ sẽ thực sự khá dễ chịu.

» Tiếp: Hydrat hóa phía máy khách
« Trước: Định tuyến và tách mã
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!