摘要:最與眾不同的是,該組件我們對(duì)外暴露了一個(gè)自定義的靜態(tài)函數(shù),因?yàn)槭墙M件的靜態(tài)函數(shù),因此我們可以在組件都沒(méi)創(chuàng)建實(shí)例之前就調(diào)用方法,但是因?yàn)檫€未創(chuàng)建實(shí)例,因此函數(shù)內(nèi)部不能訪問(wèn)。上篇文章是在回調(diào)函數(shù)中直接執(zhí)行了將對(duì)應(yīng)的組件實(shí)例傳遞。
前言
首先歡迎大家關(guān)注我的Github博客,也算是對(duì)我的一點(diǎn)鼓勵(lì),畢竟寫(xiě)東西沒(méi)法變現(xiàn),堅(jiān)持下去也是靠的是自己的熱情和大家的鼓勵(lì)。各位讀者的Star是激勵(lì)我前進(jìn)的動(dòng)力,請(qǐng)不要吝惜。
Vue同構(gòu)系列的文章已經(jīng)出到第三篇了,前兩篇文章Vue同構(gòu)(一): 快速上手與Vue同構(gòu)(二):路由與代碼分割都取得了不錯(cuò)的反響(可能是錯(cuò)覺(jué)),前兩篇文章本質(zhì)上講了如何在服務(wù)端渲染中使用Vue與Vue Router,基本的Vue全家桶中除了Vuex還沒(méi)有講,這篇文章也是圍繞這個(gè)主題來(lái)講的。
引子一直很認(rèn)同Redux作者Dan Abramov的一句話:
Flux 架構(gòu)就像眼鏡:你自會(huì)知道什么時(shí)候需要它。
其中頗有幾分“只可意會(huì)不可言傳”的感覺(jué),我們先來(lái)看看什么情況下我們需要在服務(wù)端渲染中引入Vuex?
前面的兩篇文章的例子都足夠的簡(jiǎn)單,然而實(shí)際的業(yè)務(wù)場(chǎng)景并不會(huì)如此的簡(jiǎn)單。比如我們想要渲染的是文章的列表,那我們肯定需要向數(shù)據(jù)源請(qǐng)求數(shù)據(jù)。在客戶端渲染中,這一切太稀疏平常了。你可能馬上會(huì)想到在組件的生命周期mounted方法中去請(qǐng)求異步的數(shù)據(jù)接口,然后將請(qǐng)求的數(shù)據(jù)賦值給Vue的響應(yīng)式數(shù)據(jù),Vue會(huì)自動(dòng)刷新界面,一切都是如此的完美,比如像下面的例子:
// ......省略
但是到了服務(wù)器渲染中,你想這么干是鐵定行不通了,因?yàn)樵诜?wù)端壓根就不會(huì)執(zhí)行到mounted的生命周期中,我們之前說(shuō)過(guò)在服務(wù)器端Vue的實(shí)例僅僅只會(huì)執(zhí)行生命周期函數(shù)beforeCreate和created,那么我們把數(shù)據(jù)請(qǐng)求的邏輯放置在這個(gè)兩個(gè)生命周期中是否可行呢?答案是不可以的,因?yàn)閿?shù)據(jù)請(qǐng)求的操作是異步的,我們并不能預(yù)期什么時(shí)候數(shù)據(jù)能返回。并且我們還需要考慮到,不僅服務(wù)端在渲染界面的時(shí)候需要數(shù)據(jù),客戶端也需要首屏頁(yè)面的數(shù)據(jù),因?yàn)榭蛻舳诵枰獙?duì)其進(jìn)行激活,難道我們需要分別在服務(wù)端和服務(wù)端兩次請(qǐng)求同一份數(shù)據(jù)嗎?那么無(wú)論是服務(wù)器還是數(shù)據(jù)源都會(huì)壓力陡增,肯定不是我們所希望看到的。
其實(shí)解決方案還是比較明確的:數(shù)據(jù)和組件分離,我們?cè)诜?wù)器渲染組件之前就將數(shù)據(jù)準(zhǔn)備好并放置在容器中,因此服務(wù)器渲染的過(guò)程中就可以直接從容器中拿現(xiàn)成的數(shù)據(jù)渲染。不僅如此,我們可以將該容器中的數(shù)據(jù)直接序列化,注入到請(qǐng)求的HTML中,這樣客戶端激活組件的時(shí)候,也能直接拿到相同的數(shù)據(jù)進(jìn)行渲染,不僅僅能減少相同的數(shù)據(jù)的請(qǐng)求并且還可以防止因?yàn)檎?qǐng)求數(shù)據(jù)的不相同導(dǎo)致的激活失敗從而客戶端重新渲染(開(kāi)發(fā)模式下,生產(chǎn)模式下不會(huì)檢測(cè),則激活就會(huì)出錯(cuò))。那誰(shuí)來(lái)?yè)?dān)任數(shù)據(jù)容器的職責(zé)呢,顯然就是我們今天講的Vuex了。
服務(wù)端數(shù)據(jù)預(yù)取我們接著在上一篇文章中代碼的構(gòu)建配置基礎(chǔ)上開(kāi)始我們的嘗試(文末會(huì)有代碼鏈接),首先我們來(lái)說(shuō)說(shuō)我們目標(biāo),我們借用CNode提供的文章接口,然后在界面中渲染出不同標(biāo)簽下的文章列表,不同路由標(biāo)簽之間切換可以加載不同的文章列表。我們使用axios作為Node服務(wù)端和瀏覽器客戶端通用的HTTP請(qǐng)求庫(kù)。先寫(xiě)接口, CNode給我們提供了如下的接口:
GET
URL: https://cnodejs.org/api/v1/to...參數(shù): page Number 頁(yè)數(shù)
參數(shù): tab 主題分類。目前有 ask share job good
參數(shù): limit Number 每一頁(yè)的主題數(shù)量
我們這次就選三個(gè)tab主題分別使用,分別是精華(good)、分享(share)、問(wèn)答(ask)
首先對(duì)組件提供接口:
// api/index.js import axios from "axios"; export function fetchList(tab = "good") { const url = `https://cnodejs.org/api/v1/topics?limit=20&tab=${tab}`; return axios.get(url).then((data)=>{ return data.data; }) }
作為演示我們僅渲染前20條數(shù)據(jù)。
接下來(lái)我們引入Vuex,之前兩篇文章都提到了我們需要為每次請(qǐng)求都生成新的Vue與Vue Router實(shí)例,其根本原因是防止不同請(qǐng)求之間數(shù)據(jù)共享導(dǎo)致的狀態(tài)污染。Vuex也是相同的原因,我們需要為每次請(qǐng)求都生成新的Vuex實(shí)例。
import Vue from "vue" import Vuex from "vuex" import { fetchList } from "../api" Vue.use(Vuex) export function createStore() { return new Vuex.Store({ state: { good: [], ask: [], share: [] }, actions: { fetchItems: function ({commit}, key = "good") { return fetchList(key).then( res => { if(res.success){ commit("addItems", { key, items: res.data }) } }) } }, mutations: { addItems: function (state, payload) { const {key, items} = payload; state[key].push(...items); } } }) }
這里我們假設(shè)你已經(jīng)對(duì)Vuex有所了解,首先我們調(diào)用Vue.use(Vuex)將Vuex注入到Vue中,然后每次調(diào)用createStore都會(huì)返回新的Vuex實(shí)例,其中state中包含good、ask、share數(shù)組用來(lái)存儲(chǔ)對(duì)應(yīng)主題的文章信息。 名為addItems的 mutation負(fù)責(zé)向state中對(duì)應(yīng)的數(shù)組中增加數(shù)據(jù),而名為fetchItems的action則負(fù)責(zé)調(diào)用異步接口請(qǐng)求數(shù)據(jù)并更新對(duì)應(yīng)的mutation。
那我們什么時(shí)候調(diào)用fetchItems是需要考慮一下。特定路由對(duì)應(yīng)于特定的組件,而特定的組件則需要特定數(shù)據(jù)做渲染。我們說(shuō)過(guò)的實(shí)現(xiàn)邏輯是在組件渲染前就獲取到所用的數(shù)據(jù),在純客戶端渲染的程序中我們將請(qǐng)求的邏輯放置在對(duì)應(yīng)組件的生命周期中,在服務(wù)端渲染中,我們?nèi)匀粚⒃撨壿嫹胖迷诮M件內(nèi),這樣,不僅在服務(wù)端渲染的時(shí)候通過(guò)匹配的組件就能執(zhí)行其請(qǐng)求數(shù)據(jù)的邏輯,并且在客戶端激活后,組件內(nèi)部也可以在必要的時(shí)刻中執(zhí)行邏輯去請(qǐng)求或者更新數(shù)據(jù)。我們看例子:
// TopicList.vue{{ item.title }}
Vue組件的模板不需要解釋,之所以增加button按鈕來(lái)打開(kāi)對(duì)應(yīng)文章的鏈接主要是想驗(yàn)證客戶端是否正確激活。該組件從store中獲取數(shù)據(jù),其中route的id表示文章的主題。最與眾不同的是,該組件我們對(duì)外暴露了一個(gè)自定義的靜態(tài)函數(shù)asyncData,因?yàn)槭墙M件的靜態(tài)函數(shù),因此我們可以在組件都沒(méi)創(chuàng)建實(shí)例之前就調(diào)用方法,但是因?yàn)檫€未創(chuàng)建實(shí)例,因此函數(shù)內(nèi)部不能訪問(wèn)this。asyncData內(nèi)部邏輯是觸發(fā)store中的fetchItems的action。
接下來(lái)我們看路由的配置:
import Vue from "vue" import Router from "vue-router" Vue.use(Router) export function createRouter() { return new Router({ mode: "history", routes: [{ path: "/good", component: () => import("../components/TopicListCopy.vue") },{ path: "/:id", component: () => import("../components/TopicList.vue") }] }) }
我們給good路由配置了特殊的TopicListCopy組件,他與TopicList除了名字之外,其他的全部一樣,其他的路由我們使用前面介紹的TopicList組件,之所以要這么做主要是出于方便后面介紹其中的操作。
然后我們看一下應(yīng)用的入口app.js:
import Vue from "vue" import { createStore } from "./store" import { createRouter } from "./router" import App from "./components/App.vue" export function createApp() { const store = createStore() const router = createRouter() const app = new Vue({ store, router, render: h => h(App) }) return { app, store, router } }
和之前的代碼大致相同,只不過(guò)在每次調(diào)用createApp函數(shù)的時(shí)候,創(chuàng)建Vuex的實(shí)例store,并給Vue實(shí)例注入store實(shí)例。
接下來(lái)看服務(wù)端渲染的入口entry-server.js:
// entry-server.js import { createApp } from "./app" export default function (context) { return new Promise((resolve, reject) => { const {app, store, router} = createApp() router.push(context.url) router.onReady(() => { const matchedComponents = router.getMatchedComponents() if(matchedComponents.length <= 0){ return reject({ code: 404 }) }else { Promise.all(matchedComponents.map((component) => { if(component.asyncData){ return component.asyncData({ store, route: router.currentRoute }) } })).then(()=> { context.state = store.state resolve(app) }) } }, reject) }) }
服務(wù)端的渲染入口文件和之前的結(jié)構(gòu)基本保持一致,onReady會(huì)在所有的異步鉤子函數(shù)和異步組件加載完畢之后執(zhí)行傳遞的回調(diào)函數(shù)。上篇文章是在onReady回調(diào)函數(shù)中直接執(zhí)行了resolve(app)將對(duì)應(yīng)的組件實(shí)例傳遞。但是在這里我們做了一些其他的工作。首先我們調(diào)用了router.getMatchedComponents()獲取了當(dāng)前路由匹配的路由組件,注意我們這里匹配的路由組件并不是實(shí)例而僅僅只是配置對(duì)象,然后我們調(diào)用所有匹配的路由組件中的asyncData靜態(tài)方法,加載各個(gè)路由組件所需的數(shù)據(jù),等到所有的路由組件的數(shù)據(jù)都加載完畢之后,將當(dāng)前store中的state賦值給context.state并resolve了組件實(shí)例。需要注意的是,這時(shí)store中存有首屏渲染組件所需的所有數(shù)據(jù),我們將其值賦值給context.state,renderer如果使用的是template的話,會(huì)將狀態(tài)序列化并通過(guò)注入HTML的方式存儲(chǔ)到window.__INITIAL_STATE__上。
接下來(lái)我們看瀏覽器渲染入口entry-client.js:
//entry-client.js import { createApp } from "./app" const {app, store, router} = createApp(); if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } router.onReady(() => { app.$mount("#app") })
瀏覽器激活的邏輯也和上篇文章相類似,唯一不同的是,我們?cè)谝婚_(kāi)始就調(diào)用replaceState將store中的狀態(tài)state替換成window.__INITIAL_STATE__,這樣客戶端直接可以用此數(shù)據(jù)激活避免二次請(qǐng)求。
與上一篇文章中的代碼相比,服務(wù)器的server.js代碼保持一致,沒(méi)有其他的修改。現(xiàn)在我們打包看一下我們程序的效果:
我們發(fā)現(xiàn)服務(wù)端獲取了數(shù)據(jù)渲染了文章列表并且點(diǎn)擊右側(cè)的按鈕可以打開(kāi)文章的鏈接,說(shuō)明客戶端已經(jīng)被正確的激活。但是當(dāng)我們?cè)诓煌酚芍g進(jìn)行切換的時(shí)候,發(fā)現(xiàn)其他的主題并沒(méi)有加載,這是因?yàn)槲覀冎粚?xiě)了服務(wù)端渲染中的數(shù)據(jù)獲取,而在客戶端中不同的路由切換對(duì)應(yīng)的數(shù)據(jù)加載應(yīng)該是客戶端獨(dú)立請(qǐng)求的。因此我們需要添加這部分的邏輯。
之前我們已經(jīng)說(shuō)過(guò),我們把數(shù)據(jù)請(qǐng)求的邏輯預(yù)置在組件的靜態(tài)函數(shù)asyncData中,客戶端的請(qǐng)求的走這個(gè)邏輯,那么客戶端應(yīng)該在什么時(shí)候去調(diào)用這個(gè)函數(shù)呢?
客戶端請(qǐng)求官方文檔中給出兩個(gè)思路,一個(gè)是在路由導(dǎo)航之前就解析好數(shù)據(jù)。一個(gè)是在視圖渲染后再請(qǐng)求數(shù)據(jù)
先請(qǐng)求再渲染先請(qǐng)求數(shù)據(jù),等到數(shù)據(jù)請(qǐng)求完畢之后,再渲染組件,要實(shí)現(xiàn)這個(gè)邏輯我們要借助Vue Router中的beforeResolve解析守衛(wèi),在所有組件內(nèi)守衛(wèi)和異步路由組件被解析之后,beforeResolve解析守衛(wèi)就被調(diào)用。讓我們改造一下客戶端渲染入口邏輯:
import { createApp } from "./app" const {app, store, router} = createApp(); if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } router.onReady(() => { router.beforeResolve((to, from, next) => { const matched = router.getMatchedComponents(to) const prevMatched = router.getMatchedComponents(from) // 我們只關(guān)心非預(yù)渲染的組件 // 所以我們對(duì)比它們,找出兩個(gè)匹配列表的差異組件 let diffed = false const activated = matched.filter((c, i) => { return diffed || (diffed = (prevMatched[i] !== c)) }) if (!activated.length) { return next() } // 這里如果有加載指示器(loading indicator),就觸發(fā) Promise.all(activated.map(c => { if (c.asyncData) { return c.asyncData({ store, route: to }) } })).then(() => { // 停止加載指示器(loading indicator) next() }).catch(next) }) app.$mount("#app") })
上面的beforeResolve中的代碼邏輯,首先比較to與from路由的匹配路由組件,然后找出兩個(gè)匹配列表的差異組件,再調(diào)用所有差異組件中的asyncData去獲取數(shù)據(jù),待所有數(shù)據(jù)獲取到后,調(diào)用next繼續(xù)執(zhí)行。
這時(shí)候我們打包并運(yùn)行程序,我們發(fā)現(xiàn)good切換到ask或者share是可以加載數(shù)據(jù)的,但是ask和share切換是沒(méi)法加載數(shù)據(jù)的,如下圖:
這是為什么呢?還記得我們之前專門(mén)為good路由設(shè)置了TopicListCpoy路由組件,為share與ask路由設(shè)置了TopicList路由組件,因此share與ask切換過(guò)程中而且并不存在差異組件,只是路由參數(shù)發(fā)生了變化。為了解決這個(gè)問(wèn)題,我們?cè)黾咏M件內(nèi)守衛(wèi)解決這個(gè)問(wèn)題:
beforeRouteUpdate: function (to, from, next) { this.$options.asyncData({ store: this.$store, route: to }); next() }
組件守衛(wèi)beforeRouteUpdate會(huì)在當(dāng)前路由改變,但是仍然屬于該組件被復(fù)用時(shí)調(diào)用,比如動(dòng)態(tài)參數(shù)發(fā)生改變的時(shí)候,beforeRouteUpdate就會(huì)被調(diào)用。這時(shí)我們執(zhí)行加載數(shù)據(jù)的邏輯,問(wèn)題就會(huì)得到解決。在使用先預(yù)取數(shù)據(jù),再加載組件的方式存在一個(gè)易見(jiàn)的問(wèn)題就是會(huì)感受到明顯的卡頓感,因?yàn)槟悴荒鼙WC數(shù)據(jù)什么時(shí)候能請(qǐng)求結(jié)束,如果請(qǐng)求數(shù)據(jù)時(shí)間過(guò)長(zhǎng)而導(dǎo)致組件遲遲不能渲染,用戶體驗(yàn)就會(huì)大打折扣,因此建議在加載的過(guò)程中提供一個(gè)統(tǒng)一的加載指示器,來(lái)盡量降低帶來(lái)的交互體驗(yàn)下降。
先渲染再請(qǐng)求先渲染組件再請(qǐng)求數(shù)據(jù)的邏輯比較接近與純客戶端渲染的邏輯,我們將數(shù)據(jù)預(yù)取的邏輯放置在組件的beforeMount或者mounted生命周期函數(shù)中,路由切換之后,組件會(huì)被立即渲染,但是會(huì)存在渲染組件時(shí)不存在完整數(shù)據(jù),因此這個(gè)組件內(nèi)部自身需要提供相應(yīng)加載狀態(tài)。數(shù)據(jù)預(yù)取的邏輯可以在每個(gè)路由組件多帶帶調(diào)用,當(dāng)然也可以通過(guò)Vue.mixin的方式全局實(shí)現(xiàn):
Vue.mixin({ beforeMount () { const { asyncData } = this.$options if (asyncData) { asyncData({ store: this.$store, route: this.$route }) } } })
當(dāng)然這種也會(huì)存在我們前面說(shuō)過(guò)的,路由切換但是組件復(fù)用的情況,因此僅僅只在beforeMount做操作做數(shù)據(jù)獲取是不夠的,我們?cè)诼酚蓞?shù)發(fā)生改變但是組件復(fù)用的情況下,也應(yīng)該去請(qǐng)求數(shù)據(jù),這個(gè)問(wèn)題仍然可以通過(guò)組件守衛(wèi)beforeRouteUpdate來(lái)處理。
到此為止我們已經(jīng)介紹了如何在服務(wù)器渲染中處理數(shù)據(jù)和預(yù)覽的問(wèn)題,需要看源碼的同學(xué)請(qǐng)移步到這里。如果有表達(dá)不正確的地方,歡迎指出,希望大家關(guān)注我的Github博客以及接下來(lái)的系列文章。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/108486.html
摘要:它會(huì)檢測(cè)出最大靜態(tài)子樹(shù)就是不需要?jiǎng)討B(tài)性的子樹(shù)并且從渲染函數(shù)中萃取出來(lái)。這樣在每次重渲染的時(shí)候,它就會(huì)直接重用完全相同的同時(shí)跳過(guò)比對(duì)。需要注意的是,中的操作必須是同步的,不可以存在異步操作的情況。 新增:哈哈,最近又推出了 vue 的文章,在這里放個(gè)鏈接~手把手教你從零寫(xiě)一個(gè)簡(jiǎn)單的 VUE 感謝有人看我扯技術(shù),這篇文章主要介紹最近非常火的vue2前端框架的特點(diǎn)和vue2+vuex2+we...
摘要:它會(huì)檢測(cè)出最大靜態(tài)子樹(shù)就是不需要?jiǎng)討B(tài)性的子樹(shù)并且從渲染函數(shù)中萃取出來(lái)。這樣在每次重渲染的時(shí)候,它就會(huì)直接重用完全相同的同時(shí)跳過(guò)比對(duì)。需要注意的是,中的操作必須是同步的,不可以存在異步操作的情況。 新增:哈哈,最近又推出了 vue 的文章,在這里放個(gè)鏈接~手把手教你從零寫(xiě)一個(gè)簡(jiǎn)單的 VUE 感謝有人看我扯技術(shù),這篇文章主要介紹最近非常火的vue2前端框架的特點(diǎn)和vue2+vuex2+we...
摘要:它會(huì)檢測(cè)出最大靜態(tài)子樹(shù)就是不需要?jiǎng)討B(tài)性的子樹(shù)并且從渲染函數(shù)中萃取出來(lái)。這樣在每次重渲染的時(shí)候,它就會(huì)直接重用完全相同的同時(shí)跳過(guò)比對(duì)。需要注意的是,中的操作必須是同步的,不可以存在異步操作的情況。 新增:哈哈,最近又推出了 vue 的文章,在這里放個(gè)鏈接~手把手教你從零寫(xiě)一個(gè)簡(jiǎn)單的 VUE 感謝有人看我扯技術(shù),這篇文章主要介紹最近非常火的vue2前端框架的特點(diǎn)和vue2+vuex2+we...
摘要:當(dāng)然你可以采用頁(yè)面模板的形式,將兩者相分離這里將是應(yīng)用程序標(biāo)記注入的地方中包含了模板當(dāng)然這只是最簡(jiǎn)單的一個(gè)例子,瀏覽器收到的僅僅是對(duì)應(yīng)實(shí)例的代碼,并沒(méi)有將其激活,因此是不可交互的。 前言 首先歡迎大家關(guān)注我的Github博客,也算是對(duì)我的一點(diǎn)鼓勵(lì),畢竟寫(xiě)東西沒(méi)法獲得變現(xiàn),能堅(jiān)持下去也是靠的是自己的熱情和大家的鼓勵(lì)。 同構(gòu)(服務(wù)器渲染) Vue同構(gòu)也就是我們常說(shuō)的服務(wù)器渲染(...
閱讀 3138·2021-11-24 10:24
閱讀 2929·2021-11-11 16:54
閱讀 3066·2021-09-22 15:55
閱讀 2026·2019-08-30 15:44
閱讀 1901·2019-08-29 18:41
閱讀 2761·2019-08-29 13:43
閱讀 3052·2019-08-29 12:51
閱讀 1170·2019-08-26 12:19