Web pack from scratch
Web Pack 4 is built from scratch (1)
Construction of webpack4+React16 Project (II)
Detailed partition of Web Pack 4 function configuration (3)
Introducing Ant Design and Typescript into webpack4 (IV)
Web Pack 4 code de-duplication, information simplification and construction optimization (5)
Web Pack 4 configuration Vue version scaffolding (6)
Server Rendering Series
Server Rendering - - Vue+Koa Build Successful Output Page from Zero
Server Rendering - Data Prefetching and Status
Data prefetching and status
During server-side rendering (SSR), if the application relies on some asynchronous data, it needs to pre-fetch and parse the data before starting the rendering process.
Before mounting to the client application, you need to get exactly the same data as the server application - otherwise, the client application will fail to mix because it uses a different state from the server application.
To solve this problem, the acquired data needs to be located outside the view component, that is, in a special data store or a state container.
Server-side data prefetching
vuex/index.js
We introduce Vuex state manager to synchronize data. How to obtain asynchronous data can be used according to personal needs. As long as it is compatible with both client and server, we first use timer to simulate requests.
// store.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export function createStore () { return new Vuex.Store({ state: { items: {} }, actions: { fetchItem ({ commit }, id) { // Suppose we have one that can return to Promise. // General API (please ignore the implementation details of this API) // ` store.dispatch()` will return to Promise, // So that we can know when the data will be updated. return new Promise((resolve, reject) => { setTimeout(() => { resolve({ name: 123 }) }, 500) }).then((item) => { commit('setItem', { id, item }) }) } }, mutations: { setItem (state, { id, item }) { Vue.set(state.items, id, item) } } }) }
src/app.js
Introducing vuex configuration file and using vuex-router-sync to synchronize routing status, we can get some routing information from store.
// app.js import Vue from 'vue' // Sync vue-router's current $route as part of vuex store's state. import { sync } from 'vuex-router-sync' import App from './App.vue' import createRouter from './router' import createStore from './vuex' export default function createApp () { // Create router instances const router = createRouter() const store = createStore() // Route state to store sync(store, router) const app = new Vue({ // Inject router to root Vue instance router, store, render: (h) => h(App) }) // Return app and router return { app, router, store } }
page/view1.vue
Because server rendering is also called first screen rendering, that is, we should only send the current page to the client, and similarly we should only get the data needed for the page, so we put the starting point of the request at the routing component level.
A custom static function asyncData is exposed in the component because it is called before the component is instantiated, so it cannot access this, but we can pass in route s and store s as parameters to get the information we need.
<template> <div> <p>Page1</p> <p>{{item.time}}</p> </div> </template> <script> export default { asyncData({ store, route }) { // When action is triggered, Promise is returned return store.dispatch('fetchItem', route.params.id) }, computed: { // Get item from the store's state object. item() { return this.$store.state.items[this.$route.params.id] } } }; </script>
page/view2.vue
Fair and reasonable
<template> <div> <p>Page2</p> <p>{{item.time}}</p> </div> </template> <script> export default { asyncData({ store, route }) { // When action is triggered, Promise is returned return store.dispatch('fetchItem', route.params.id) }, computed: { // Get item from the store's state object. item() { return this.$store.state.items[this.$route.params.id] } } }; </script>
entry/entry-server.js
This is where the server gets the matching routing logic, so we also call the static function to get the matching routing after we get the matching routing.
Because this is an unscheduled asynchronous operation, it is necessary to ensure that all matching components are successfully invoked through Promise.all before proceeding with the next step, remember to add the catch error operation.
This ensures that the data is pre-fetched and then filled into the store for rendering.
import createApp from '../src/app' export default (context) => { // Because it may be an asynchronous routing hook function or component, we will return a Promise. // So that the server can wait for all the content before rendering. // It's ready. return new Promise((resolve, reject) => { const { app, router, store } = createApp() // Setting the location of server-side router router.push(context.url) // Wait until router parses the possible asynchronous components and hook functions router.onReady(() => { const matchedComponents = router.getMatchedComponents() // Unmatched routes execute reject function and return 404 if (!matchedComponents.length) { return reject({ code: 404 }) } // Call `asyncData() for all matching routing components` Promise.all( matchedComponents.map((Component) => { if (Component.asyncData) { return Component.asyncData({ store, route: router.currentRoute }) } }) ) .then(() => { // After all preFetch hook resolves, // Our store is now populated with the state required by the rendering application. // When we attach state to context, // And when the `template'option is used for renderer, // The state is automatically serialized as `window. _INITIAL_STATE_', and HTML is injected. context.state = store.state // Promise should resolve the application instance so that it can render resolve(app) }) .catch(reject) }, reject) }) }
CreateBundle Renderer automatically serializes the additional context data into window. _INITIAL_STATE_ and injects HTML
entry/entry-client.js
Server rendering has serialized the store and assigned it to the window. _INITIAL_STATE_ field of the page. Then we can get it before client rendering, and then call replaceState to directly cover the client's store to achieve the goal of sharing state between front and back ends.
import createApp from '../src/app' const { app, router, store } = createApp() // Mount data if (window.__INITIAL_STATE__) { // Replace the root state of the store with state merge or time travel debugging only. store.replaceState(window.__INITIAL_STATE__) } // Called when routing completes initial navigation router.onReady(() => { // Mount the root element in the App.vue template app.$mount('#app') })
router/index.js
Synchronize the routing configuration to get the id
import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export default function createRouter () { return new Router({ // Remember to add the mode attribute, because the content after # will not be sent to the server, and the server does not know which route to request. mode: 'history', routes: [ { // home page alias: '/', path: '/view1:id', component: () => import('../page/view1.vue') }, { path: '/view2:id', component: () => import('../page/view2.vue') } ] }) }
src/App.vue
Jump routing with parameters
<template> <div id="app"> <h2>Welcome to SSR Render Page</h2> <router-link to="/view1:1">view1</router-link> <router-link to="/view2:2">view2</router-link> <router-view></router-view> </div> </template> <script> export default {}; </script>
Build files
Run command
yarn build yarn start
Direct Access Address
http://localhost:3005/view1:1
You can see that the data is already available on the interface, but there's still a problem if we click view2.
Server-side acquisition has been completed, so next we need to solve the problem of client-side data acquisition.
Client Data Fetching
There are two ways
Parsing data before navigation
The disadvantage is that the user experience is not good if the process is processed for a long time. The conventional operation is to give a loading graph to ease the user's emotions. So we can:
- Check matching components
- Screening Differential Routing Components
- Global routing hook executes asyncData function
- Mount components
import createApp from '../src/app' const { app, router, store } = createApp() // Mount data if (window.__INITIAL_STATE__) { // Replace the root state of the store with state merge or time travel debugging only. store.replaceState(window.__INITIAL_STATE__) } // Called when routing completes initial navigation router.onReady(() => { // Add routing hook function to process asyncData. // After the initial routing resolve, // So that we don't double-fetch existing data. // Use `router.beforeResolve()', to ensure that all asynchronous components are resolve d. router.beforeResolve((to, from, next) => { // Returns the target location or an array of components matching the current route (the definition / constructor class of the array, not the instance). Usually used when pre-loading data rendered on the server side. const matched = router.getMatchedComponents(to) const prevMatched = router.getMatchedComponents(from) // We only care about non-prerendered components // So let's compare them and find out the difference between the two matching lists. let diffed = false const activated = matched.filter((component, index) => { return diffed || (diffed = prevMatched[index] !== component) }) if (!activated.length) { return next() } // Here, if there is a load indicator, it triggers Promise.all( activated.map((component) => { if (component.asyncData) { return component.asyncData({ store, route: to }) } }) ) .then(() => { // Stop loading indicator next() }) .catch(next) }) // Mount the root element in the App.vue template app.$mount('#app') })
Rerun the code and you can see that it's working properly, because when you set the timer 500 milliseconds in front of you, there's a clear sense of karton.
The final code can view the warehouse Vue-ssr-demo/demo2
Match the view and retrieve the data
Place client-side data prefetching logic in the beforeMount function of the view component. When routing navigation is triggered, views can be switched immediately, so the application has faster response speed. However, the incoming view will not have complete data available for rendering. Therefore, for each view component that uses this strategy, a conditional loading state is required.
To put it plainly, just like ordinary calls, you need to have a default state rendering view, which renders the interface again after getting the data.
We can use the beforeMount lifecycle, which has completed initializing data and el data, compiling templates, etc., but has not yet been mounted on the BOM node. Although we can not directly access the current instance of the component, we can access custom properties through this.$options.
page/view1.vue
<template> <div> <p>Page1</p> <p>{{item ? item.time : ''}}</p> </div> </template> <script> export default { asyncData({ store, route }) { // When action is triggered, Promise is returned return store.dispatch('fetchItem', route.params.id) }, beforeMount() { if (this.$options.asyncData) { // Assigning the fetch data operation to promise // So in the component, we can prepare the data when it's ready. // Perform other tasks by running `this.dataPromise.then(...)'. this.$options.asyncData({ store: this.$store, route: this.$route }) } }, computed: { // Get item from the store's state object. item() { return this.$store.state.items[this.$route.params.id] } } }; </script>
page/view2.vue
<template> <div> <p>Page2</p> <p>{{item ? item.time : ''}}</p> </div> </template> <script> export default { asyncData({ store, route }) { // When action is triggered, Promise is returned return store.dispatch('fetchItem', route.params.id) }, beforeMount() { if (this.$options.asyncData) { // Assigning the fetch data operation to promise // So in the component, we can prepare the data when it's ready. // Perform other tasks by running `this.dataPromise.then(...)'. this.$options.asyncData({ store: this.$store, route: this.$route }) } }, computed: { // Get item from the store's state object. item() { return this.$store.state.items[this.$route.params.id] } } }; </script>
Rerun the code to find that it's working properly, the page responds quickly, and then it delays switching data a little bit.
The final code can view the warehouse Vue-ssr-demo/demo3