1. 程式人生 > >從壹開始前後端分離 [ vue + .netcore 補充教程 ] 三十║ Nuxt實戰:動態路由+同構

從壹開始前後端分離 [ vue + .netcore 補充教程 ] 三十║ Nuxt實戰:動態路由+同構

上期回顧

說接上文《二九║ Nuxt實戰:非同步實現資料雙端渲染》,昨天咱們通過專案二的首頁資料處理,簡單瞭解到了 nuxt 非同步資料獲取的作用,以及親身體驗了幾個重要資料夾的意義,整篇文章也一直在往如何實現服務端渲染的方向講解,因為我個人感覺這個是一個重點,如果是隻會如何使用的話,大家就可以走馬觀花的看看就行了,昨天呢,遺留了幾個問題,我也想了想,還沒有想好如何通過淺顯的話來概括,如果要是搬出來教科書似的講解,感覺又不是很清晰,我就在以後的領悟中補充吧,這裡就先說下其中的三個問題:

1、我們通過 dev 編譯,生成的 .nuxt 臨時資料夾(我個人感覺他就像我們 .net core 中的 bin 資料夾),.nuxt 目錄為 npm run dev或者是npm run build 後才生成,兩個操作都執行了 build() 方法,用於存放 Nuxt.js 的核心庫檔案,如果你將一個老專案的 .nuxt 資料夾覆蓋一個新專案的 .nuxt 資料夾,新專案正常執行,按照老的專案路由規則之類的都可以正常訪問。例如,你可以在這個目錄下找到 server.js

 檔案,描述了 Nuxt.js 進行服務端渲染的邏輯,流程是:呼叫 nuxtServerInit 方法,當請求打入時,最先呼叫的即是 nuxtServerInit 方法,可以通過這個方法預先將伺服器的資料儲存,如已登入的使用者資訊等。另外,這個方法中也可以執行非同步操作,並等待資料解析後返回。Middleware 層,經過第一步後,請求會進入 Middleware 層,在該層中有三步操作:讀取 nuxt.config.js 中全域性 middleware 欄位的配置,並呼叫相應的中介軟體方法 匹配並載入與請求相對應的 layout 呼叫 layout 和 page 的中介軟體方法。呼叫 validate
 方法,在這一步可以對請求引數進行校驗,或是對第一步中伺服器下發的資料進行校驗,如果校驗失敗,將丟擲 404 頁面。

呼叫 fetch 及 asyncData 方法,這兩個方法都會在元件載入之前被呼叫,它們的職責各有不同, asyncData 用來非同步的進行元件資料的初始化工作,而 fetch 方法偏重於非同步獲取資料後修改 Vuex 中的狀態。

2、每次修改檔案,都會觸發熱 webpack 的[HMR] 熱載入,因為 Nuxt.js集成了如下模組: Vue-Router, Vue-Meta 和 Vuex (僅在使用 Vuex 狀態樹配置項 時引入)。 這樣的好處在於,不需要手工配置依賴,每次當我們修改檔案,webpack 就會自動儲存,Nuxt.js 使用 Webpack 和 vue-loader 、 babel-loader 來處理程式碼的自動化構建工作(如打包、程式碼分層、壓縮等等)。

 

4、在 network 中,當有一個請求過來時,伺服器會新建一個vue例項,渲染(render)出需要顯示的頁面的html,把得到的頁面以字串的形式返回給客戶端。同時把相關的js檔案也返回(首次請求時返回vue的runtime、webpack的runtime和app.js等檔案,非首次請求返回按需載入的js檔案),返回的js檔案和單頁面應用(SPA)返回的差不多

app.js:基本就是你實際編寫的那個app.vue(.vue或.js),沒這個頁面跑不起來,該頁面應該提供了跟app應用相關的公共方法,腳本里也明確配置了跟路由相關的資訊

vendor.js:vue-cli全家桶預設配置裡面這個chunk就是將所有從node_modules/裡require(import)的依賴都打包到這裡,所以這個就是所有node_modules/下的被require(import)的js檔案

manifest.js: 最後一個chunk,被注入了webpackJsonp的定義及非同步載入相關的定義(webpack呼叫CommonsChunkPlugin處理後模組管理的核心,因為是核心,所以要第一個進行載入,不然會報錯),該檔案確定是跟路由相關的配置資訊,其中明確包含了路由的路徑,和版本號,但是暫時不明白為何前端輸出會保留該配置(大概是做一些頁面動態切換效果或者是預載入的時候使用,但是頁面的預載入已經在ssr 輸出的html 已經包含了)

然後還有一些 pages_index.js檔案,佈局 layouts_blog.js檔案等:default.js(跟dis/layout/default.js一致,是載入了使用的layout)

。瀏覽器接收到這些檔案後,通過js檔案把靜態頁面的字串hydrate成可以互動的應用。和SPA相比,SSR返回的資料就是多了個靜態頁面(字串形式)。

 

我又一次老生常談的說了一遍,還是感覺不是很清晰,看來自己的功底還是不行呀,如果有愛好 nuxt 或者 做過 SSR 的小夥伴,歡迎聯絡,咱們一起討論下,今天呢,接著昨天的工作,把詳情頁渲染出來吧~~~

零、今天要完成紫色的部分

 

一、動態路由實現詳情頁佈局設計

經過昨天的首頁渲染,大家不知道使用起來怎麼樣,不僅可以配置每一頁的 head 資訊( TDK head),還可以對整體進行配置,雖然中間引入了 plugins 外掛機制,不過也是很好的做了封裝,特別是路由這一塊,大家是不是發現已經完全不用配置了,Nuxt.js 依據 pages 目錄結構自動生成 vue-router 模組的路由配置,為我們減少了很大的工作量,今天咱們就繼續對詳情頁進行配置。

1、什麼是動態路由

昨天呢,咱們開發了首頁,通過地址直接可以訪問,但是在開發過程中,肯定會有這樣的頁面:通過不同的 id 載入不同的詳情頁面,這些頁面雖然是一個,但是 URL 地址卻是多個,所以我們就說這個路由是動態的,還記得咱們在第一個專案中的時候,是怎麼配置的麼?我們通過頁面接收引數來實現動態路由

    {
      path: "/Content/:id",
      name: "Content",
      component: Content
    },

在 Nuxt.js 裡面定義帶引數的動態路由,需要建立對應的以下劃線作為字首的 Vue 檔案 或 目錄。

以下目錄結構:

pages/
--| _slug/
-----| comments.vue
-----| index.vue
--| users/
-----| _id.vue
--| index.vue

Nuxt.js 生成對應的路由配置表為:

router: {
  routes: [
    {
      name: 'index',
      path: '/',
      component: 'pages/index.vue'
    },
    {
      name: 'users-id',
      path: '/users/:id?',
      component: 'pages/users/_id.vue'
    }
  ]
}

你會發現名稱為 users-id 的路由路徑帶有 :id? 引數,表示該路由是可選的。如果你想將它設定為必選的路由,需要在 users/_id 目錄內建立一個 index.vue 檔案。

2、新增部落格詳情頁

1、在 pages 資料夾中,新增 blog 資料夾,然後新增 _id.vue 頁面

這個時候,我們看我們的臨時編譯檔案 .nuxt 中 router.js 已經動態的增加上了上邊新增的路由

 return new Router({
    mode: 'history',
    base: '/',
    linkActiveClass: 'nuxt-link-active',
    linkExactActiveClass: 'nuxt-link-exact-active',
    scrollBehavior,
    routes: [
        {
            path: "/blog/:id?",
            component: _66cb1a63,
            name: "blog-id"
        },
        {
            path: "/",
            component: _70e72bdd,
            name: "index"
        }
    ],

2、編輯 _id.vue 檔案,實現資料獲取

<template>
  <div class="post-page">
    <h1 class="title">{{data.btitle}}</h1>
    <p class="createTime">{{data.bCreateTime }}</p>
    <div  v-html="data.bcontent"  ></div>
  </div>
</template>

<script>
  import Vue from "vue";
  export default {
    layout: "blog",
    validate ({ params }) {
      // 校驗文章id是否為數字
      return /^\d+$/.test(params.id);
    },
    async asyncData ({ params, error }) {
      // 獲取文章詳情
      let data = {};
      try {
        data = await Vue.http.get(`blog/${params.id}`);
        return {
          data: data
        };
      } catch (e) {
        //error({ statusCode: 404, message: "出錯啦" });
      }
    },
    fetch ({ store, params }) {},
    data () {
      return {
        comments: []
      };
    },
    head () {//設定頁面 head 資訊
      return {
        title: `${this.data.btitle}`,
        meta: [
          {
            name: "description",
            content: this.data.btitle
          }
        ]
      };
    },
    filters: {
      timeFormat: function (time) {
        if (!time) return "";
        return time;
      }
    },
    mounted () {},
    components: {
    }
  };
</script>
//匯入樣式
<style lang="css">
  @import "../../static/vue-blog-sq.css";
</style>

是不是很簡單,直接新增頁面內容,就可以實現路由渲染,直接就可以訪問了,不過這裡可能會有一個坑,如果你運氣好的話,會碰上,運氣不好,就過去了。

3、重新整理頁面檢視結果

蒼天呀,不是吧,報錯了?!如果你看到這個錯誤,恭喜你比較幸運,可能你會進一步的瞭解到 nuxt 是如何渲染的。

4、點選 瀏覽器後退 ,返回到首頁,發現更加崩潰

 

不僅剛剛的詳情頁不見了,就連我們的首頁資料也出錯了!雖然這上面有資料,但是這個是瀏覽器快取的,而不是我們真實的資料,這個時候著急的小夥伴,一定會很著急,穩住,我們能贏!

3、首次執行服務端渲染,然後開始客戶端渲染

這個時候,如果你重新整理首頁,發現一切正常,不僅如何,如果你重新整理詳情頁,資料也能出現,不信你可以試試,那這是為什麼呢?

原因就在於我們重新整理頁面,或者新視窗開啟等等,都是新開了一個服務,我們的頁面為了實現 SEO 先進行的是服務端渲染,講整個頁面的字串傳送過來,然後點選連結去詳情頁的時候,我們就開始走客戶端渲染了,之所以頁面會報錯,就是我們存在跨域的問題。

你可能會問,問什麼第一次不存在,因為第一次是服務端渲染呀,服務端是不存在跨域問題的,只有 js 請求才會存在跨域的問題,到這裡,通過這個錯誤你是不是瞭解到了一點兒,這個錯誤也是我故意放出來的,就是為了讓大家更清楚的瞭解到 nuxt 是如何進行渲染的。這也能說的通,為什麼第一次重新整理首頁有資料,從詳情頁返回過來,報錯的原因了,因為第二次渲染已經交給客戶端了。


解決辦法很簡單,還是在我們 .net core api 中 CORS 跨域配置我們的埠就行,然後一切正常了。 

相信這個時候你對 nuxt 的渲染有了一點理解了吧,如果還不是很清晰,請往下看

二、SSR 同構知多少

SSR 用通過同構的方法解決了上面問題。我們先說一下 SSR 的具體表現,比如我們現在有一個列表頁,列表中每一行對應一個詳情頁,那麼如果直接用瀏覽器訪問列表頁時,伺服器返回資料和 html 融合後的頁面,瀏覽器拿到頁面直接渲染,這就省去了先請求 js 再由 js 發起資料請求的過程,頁面渲染的同時請求js,js載入完成後繫結事件;從列表頁中點選某一條到詳情頁的時候,和普通的全棧 Ajax 一樣,先請求 js 再由 js 發起資料請求,然後填充資料渲染頁面。如果將詳情頁的連結複製出來,直接在新瀏覽中訪問,那麼詳情頁會直接返回資料和 html 融合後的頁面(服務端渲染),渲染的同時請求詳情頁 js,最後再繫結事件。這個“伺服器端拼接 html 和 html 是由同樣的頁面和元件完成的,這種前後端採用同樣的結構在不同的環境中產出同樣的 html 的方案稱之為“同構”。

1、什麼叫前後端同構?

為了解決某些問題(比如SEO、提升渲染速度等)vue 提供了2個方法在服務端生成一個HTML文字格式的字串。在得到了這個HTML格式的字串之後,通常會將其組裝成一個頁面直接返回給使用者的瀏覽器。

到這裡,服務端的活已經幹完了,然後就是瀏覽器這邊幹活。

瀏覽器拿到HTML文字後,立刻進行渲染將內容呈現給使用者。然後載入頁面所需的 .js 檔案,然後執行 JavaScript 指令碼,然後開始初始化 vue 元件

到這裡問題就來了。vue 初始化元件後會執行元件內所有 render () 方法,然後生成虛擬DOM的樹形結構,然後在適當的時候將虛擬dom寫到瀏覽器的真實 dom 中。因為 vue 總是根據虛擬 dom 來生成真實dom,所以最後會把伺服器端渲染好的HTML全部替換掉。

上面這個事情說不是問題確實也不是問題,無非就是使用者看到頁面然後“閃現”一下。說是問題還真是個問題,產品會拿著這毛病從使用者體驗的角度在各種場合和你死磕半個月。磕累了你索性把服務端渲染關了,然後運營又拿著SEO的問題準備和你開始撕逼了。

為了解決這些問題,他們在 .renderToString(element) 方法中提供了一個 checksum 機制。前後端同構就是保證前端和後端的dom結構一致,不會發生重複渲染。

2、什麼叫 首屏渲染?

簡單的說就是 vue 在瀏覽器記憶體中第一次生成的虛擬 dom 樹。切記是虛擬 dom ,而不是瀏覽器的dom。

瞭解 vue 的應該知道,所有 vue元件都有一個 render() 方法(如果使用function方式編寫的元件會把function裡的所有程式碼都塞到 render() 方法中去)。當 render( element, container, [callback] )方法執行時,會執行以下步驟:

1. 所有元件的會先進行初始化(es6執行建構函式)。
2. 所有元件的 render () 方法會被呼叫一次,完成這個過程後會得到一顆虛擬的 dom 樹。
3. vue 會將虛擬dom轉換成瀏覽器dom,完成後呼叫元件的 componentDidMount() 方法告訴你已經裝載到瀏覽器上了。

在上面這個過程成中,步驟2完成後即為完成 vue 的首屏渲染。結合 checksum 機制步驟3有可能不會執行。

當元件狀態發生變更時( setState() 生命週期函式被呼叫)或者 父元件渲染時(父元件的 render() 方法被呼叫),當前元件的 render() 方法都會被執行,都有可能會導致虛擬dom變更,但是這些變更和首屏渲染沒任何關係了。

3、檢視是如何渲染的

 1、在我們的首頁中,首次載入,在 network 中,檢視我們都載入了那些檔案

這些檔案咱們在文章頂部都講到了,這裡說下 初始頁面,它是直接將 html 返回給我們的前端渲染,這個很好理解

2、點選到詳情頁

我們發現這個我們的網路請求,並沒有繼續打包 build 走服務端渲染,而是僅僅請求了一個介面,返回了 json 資料,從這裡大家應該就能看的處理,這就是所謂的雙端渲染模式。

三、總結

好啦,今天就暫時說到這裡了,通過詳情頁的新增,大家會切身體會到 nuxt 的渲染模式,是如何在服務端和客戶端之間來回切換渲染的,這三篇文章大家要多看看,才能瞭解其中的內涵,加油鴨~~

四、Github