1. 程式人生 > >從壹開始前後端分離 [ vue + .netcore 補充教程 ] 二九║ Nuxt實戰:非同步實現資料雙端渲染

從壹開始前後端分離 [ vue + .netcore 補充教程 ] 二九║ Nuxt實戰:非同步實現資料雙端渲染

回顧

哈嘍大家好!又是元氣滿滿的周~~~二哈哈,不知道大家中秋節過的如何,馬上又是國慶節了,博主我將通過三天的時間,給大家把專案二的資料添上(這裡強調下,填充資料不是最重要的,最重要的是要配合著讓大家明白 nuxt.js 是如何一步步實現服務端渲染的),雖說是基於 Nuxt 的,但是資料來源還是我們的老資料,就是 .net core api 的資料,和上一個專案中使用的是一樣的,這裡要說下,有的小夥伴說想要我的資料,這裡暫時說抱歉,因為裡邊有一些我的私人的記錄,還有一些網站密碼啥的,我自己懶得一條一條的刪除了,這裡就不放出來了,多多包涵,不過介面地址可以隨便使用http://123.206.33.109:8081

,有原始碼和資料庫表結構,相信大家還是可以搞定滴,因為是三天,所以今天我們就會把首頁給處理出來,這裡有倆個點,第一,為什麼是三天呢,因為博主三天後要放假了哈哈( 這裡要給大家說下心得,堅持學習和堅持寫部落格是完全不同的時間量,每天我光寫部落格的時間最少三個小時,加上工作的九個小時,每天我至少需要12個小時,所以如果想快速學習,建議還是要好好的寫部落格 ),第二,資料獲取和之前的 vue 有點兒差別,雖然都是基於 axios ,但是 nuxt 框架做了一定的封裝,而且還是非同步的,ASync/Await ,大家看我今天的標題也能看的出來,至於為什麼會是非同步的呢,先留個神祕,大家看完今天的講解應該就知道是為什麼了~

書接上文,上週咱們說到了nuxt 的執行原理《二八║ Nuxt 基礎:面向原始碼研究Nuxt.js》,不知道大家是否看了呢,主要通過原始碼的分析,來重點說明 Nuxt 是如何實現服務端渲染的,個人感覺寫的比較羞澀難懂,我也是在慢慢的潤色,儘量修改成通俗易懂的給大家展示,寫成人話。這裡我要說句題外話,大家有時間的話,還是應該把後端的注意力拿出來一點兒點兒放到前端了,以前我也是一個老後端,一直想著各種持久化ORM哪個更帥,各種框架哪個價效比更高,但是一直也技術平平,反而忽略了這兩年的前端發展,經過這一個月的學習,我發現前端技術竟然發展如此之快,竟超出我的想象,有點兒追趕挑戰後端的意思了,多語言化的發展,更有助於一個程式設計師的發展( 這個歡迎來噴,只會一種語言的話,嗯~ 會有侷限性┑( ̄Д  ̄)┍ ),哈哈這個扯的有點兒遠了。

對於昨天的文章,總結來說,nuxt 的核心就是在 vue.js 的基礎上,封裝了雙端渲染模式(服務端和客戶端),結合頁面html片段快取,來實現 SSR ,最終解決首屏快速渲染和 SEO 的問題,核心就是在如何實現雙端渲染上,大家之前的教程中,對 Vue 的 SPA 很熟練了,這三天咱們就慢慢的研究下,如何實現雙端渲染的,這個也就是我講 nuxt 的核心 —— 渲染。

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

 

一、重點溫習框架中的幾個部分檔案 —— 鋪墊

1、nuxt.config.js 檔案

專案的核心檔案,作為一個配置檔案,它配合著 app.html 一起擔當著之前我們的 index.html 的作用,只不過這裡是更偏重於配置,對全域性配置起到一個十分重要的作用,像我們的 webconfig 一樣

//這些配置在你的專案裡可能不一定都有,但是我都會提到
module.exports = {
  cache: {},
  css: [
    // 載入一個 node.js 模組
    //  'hover.css/css/hover-min.css',
    //  // 同樣載入一個 node.js 模組,不過我們定義所需的前處理器
    //  { src: 'bulma', lang: 'sass' },
    //  // 專案中的 CSS 檔案
    //  '~assets/css/main.css',
    //  // 專案中的 Sass 檔案
    //  { src: '~assets/css/main.scss', lang: 'scss' } // 指定 scss 而非 sass
  ],
  // 預設 true
  dev: process.env.NODE_ENV !== 'production',//不是生產環境

  // 建立環境變數
  env: {},

  // 配置 Nuxt.js 應用生成靜態站點的具體方式。
  genetate: {
    dir: '',
    minify: '',
    routes: [],
  },
  /*
    * vue-meta
    * Headers of the page
    */
  head: {
    title: '老張的哲學',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'Nuxt.js project' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  /*
  ** Customize the progress bar color
  */
  loading: { color: '#3B8070' },
  /*
  ** Build configuration
  */
  build: {
    /*
    ** Run ESLint on save
    */
    extend (config, { isDev, isClient }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  },
  performance: {
    gzip: false,
    prefetch: true
  },
  // 引入 Vue.use 的外掛
  plugins: [],
  // 預設當前路徑
  rootDir: process.cwd(),
  router: {
    base: '',
    mode: 'history',
    linkActiveClass: 'nuxt-link-active',
    scrollBehavior: (to, from, savedPosition) => {
      // savedPosition 只有在 popstate 導航(如按瀏覽器的返回按鈕)時可以獲取。
      if (savedPosition) {
        return savedPosition
      } else {
        let position = {}
        // 目標頁面子元件少於兩個
        if (to.matched.length < 2) {
          // 滾動至頁面頂部
          position = { x: 0, y: 0 }
        }
        else if (to.matched.some((r) => r.components.default.options.scrollToTop)) {
          // 如果目標頁面子元件中存在配置了scrollToTop為true
          position = { x: 0, y: 0 }
        }
        // 如果目標頁面的url有錨點,  則滾動至錨點所在的位置
        if (to.hash) {
          position = { selector: to.hash }
        }
        return position
      }
    },
    // default
    middleware: 'user-agent',
    // 擴充套件路由
    extendRoutes: () => {},

    // 默認同 rootDir
    srcDir: this.rootDir,

    transition: {
      name: 'page',
      mode: 'out-in'
    },
    watchers: {
      chokidar: {}, // 檔案監控
      webpack: {
        aggregateTimeout: 300,
        poll: 1000
      }
    }
  }
}

2、檢視模板

預設的 html 模版: 應用根目錄下的 app.html 檔案, 如果你沒有找到這個檔案, 則採用預設的模版,當然你也可以自己新增,配置,

這個更像是我們之前的 index.html 頁面,它配合著 nuxt.config.js 一起作為專案承載頁面,更偏重於模板,把 <div id='app'></div> 掛載,變成了填充的方式。

<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head>
    {{ HEAD }}
  </head>
  <body {{ BODY_ATTRS }}>
    {{ APP }}
  </body>
</html>

3、layouts 佈局目錄

可以修改該目錄下的 default.vue 來修改預設佈局 , 這個就是類似於我們之前的 app.vue 頁面,

<template>
  <div class="layout-default">
    <cl-header></cl-header>
    <nuxt class="layout-main"/>
    <cl-footer></cl-footer>
    <div class="layout-bg"></div>
  </div>
</template>

其中 <nuxt/> 是必需的,頁面的主體內容會顯示在這裡 (類似於根節點的 <router-view/>)

此外還可以在目錄下新增 error.vue 作為錯誤頁面,具體的寫法可以參考官方文件

4、pages 頁面 路由

路由, 約定大於配置, 支援動態, 巢狀, 動態巢狀路由, 過渡效果和中介軟體,通過資料夾目錄名稱, 元件名稱, 生成路由配置,預設的 transitionName 為 page, 可在 assets 中新增全域性的過渡效果,

在匹配頁面之前執行;

頁面元件實際上是 Vue 元件,只不過 Nuxt.js 為這些元件添加了一些特殊的配置項(對應 Nuxt.js 提供的功能特性)以便你能快速開發通用應用。

<template>
  <h1 class="red">Hello {{ name }}!</h1>
</template>

<script>
export default {
  asyncData (context) {
    // called every time before loading the component
    return { name: 'World' }
  },
  fetch () {
    // The fetch method is used to fill the store before rendering the page
  },
  head () {
    // Set Meta Tags for this Page
  },
  // and more functionality to discover
  ...
}
</script>

<style>
.red {
  color: red;
}
</style>

nuxt.config.js --> 執行middleware --> 匹配佈局 --> 匹配頁面

用於存放頁面級別的元件,nuxt 會根據該目錄下的頁面結構生成路由

比如上圖中的頁面結構,會生成這樣的路由配置:

const _7b01ffaa = () => import('..\\pages\\post\\index.vue' /* webpackChunkName: "pages_post_index" */).then(m => m.default || m)
const _2b7fe492 = () => import('..\\pages\\post\\_id.vue' /* webpackChunkName: "pages_post__id" */).then(m => m.default || m)
const _4f14dfca = () => import('..\\pages\\index.vue' /* webpackChunkName: "pages_index" */).then(m => m.default || m)

export function createRouter () {
  return new Router({
    mode: 'history',
    base: '/',
    linkActiveClass: 'nuxt-link-active',
    linkExactActiveClass: 'nuxt-link-exact-active',
    scrollBehavior,
    routes: [
        {
            path: "/post",
            component: _7b01ffaa,
            name: "post"
        },
        {
            path: "/post/:id",
            component: _2b7fe492,
            name: "post-id"
        },
        {
            path: "/",
            component: _4f14dfca,
            name: "index"
        }
    ],
    
    
    fallback: false
  })
}

5、使用外掛 plugins 資料夾

如果專案中還需要引入其他的第三方外掛,可以直接在頁面中引入,這樣在打包的時候,會將外掛打包到頁面對應的 js 裡面,但要是別的頁面也引入了同樣的外掛,就會重複打包。如果沒有需要分頁打包的需求,這時候可以配置 plugins,然後在根目錄的 nuxt.config.js 中新增配置項 build.vendor 和 plugins,這裡的 plugins 屬性用來配置 vue.js 外掛,也就是 可以用 Vue.user() 方法 的外掛

預設只需要 src 屬性,另外還可以配置 ssr: false,讓該檔案只在客戶端被打包引入,如果是像 axios 這種第三方 (不能 use) 外掛,只需要在 plugins 目錄下建立 axios.js,然後在 build.vendor 中新增配置 (不需要配置 plugins)

這樣在打包的時候,就會把 axios 打包到 vendor.js 中。

二、配置頁面,實現首頁的資料獲取 —— 非同步

1、在 static 檔案中,新增樣式 vue-blog-sq.css 檔案

提醒,這裡的檔案,不會被打包,所以會在頁面中呈現原有格式,如果想每次都被打包壓縮,需要寫到 assets 資原始檔夾中

2、在 components 中,新建 layout 資料夾,然後新增頁頭頁尾 元件

這個很簡單,就是普通的 *.vue 元件寫法,大家可以自行下載瀏覽

3、在 layouts 頁面佈局資料夾中,新增 blog.vue 佈局

提醒:以後也可以單給使用者增加布局,比如 user.vue 

<template>
  <div class="layout-default">
    <cl-header></cl-header>
    <nuxt class="layout-main"/>//注意,<nuxt />,必須有,類似一個 <router-view/>
    <cl-footer></cl-footer>
  </div>
</template>

<script type="text/javascript">
  import clHeader from "~/components/layout/header.vue";
  import clFooter from "~/components/layout/footer.vue";
  export default {
    data () {
      return {};
    },
    mounted () {
    },
    components: {
      clHeader,
      clFooter
    }
  };
</script>

很簡單的一個佈局入口,是不是很像我們之前的 App.vue 中的 路由容器 —— <router-view /> ,但是又比其更豐富,因為我們之前的 app.vue 只能有一個入口,但是 nuxt 可以提供多個自定義 模板layouts,更方便。

4、根目錄新增 config 資料夾,新增 index.js ,作為我們以後的配置檔案,類似 .net core api 中的 appsetting.json 檔案

const config = {
  //開發環境配置,開發的時候
    development: {
         //api: "http://localhost:58427/api/",
        api: "http://123.206.33.109:8081/api/"
    },
  //生產環境配置,部署的時候
    production: {
        api: ""
    }
};
//獲取當前環境變數,是 production或者development
module.exports = config[process.env.NODE_ENV];

5、在 plugins 外掛中,新增 server_site 資料夾,然後新增 http.js 和 index.js

1、為什麼要使用外掛?

我們可以在應用中使用第三方模組,一個典型的例子是在客戶端和服務端使用 axios 做 HTTP 請求。

首先我們需要安裝 npm 包:

npm install --save axios

然後在頁面內可以這樣使用:

<template>
  <h1>{{ title }}</h1>
</template>

<script>
import axios from 'axios'

export default {
  async asyncData ({ params }) {
    let { data } = await axios.get(`https://my-api/posts/${params.id}`)
    return { title: data.title }
  }
}
</script>

有一個值得注意的問題是,如果我們在另外一個頁面內也引用了 axios,那麼在應用打包釋出的時候 axios 會被打包兩次,而實際上我們只需要打包一次。這個問題可以通過在 nuxt.config.js 裡面配置 build.vendor 來解決:

module.exports = {
  build: {
    vendor: ['axios']
  }
}

經過上面的配置後,我們可以在任何頁面裡面引入 axios 而不用擔心它會被重複打包。

2、為什麼要分 server_site服務端 與 client_site客戶端 外掛

有些外掛可能只是在瀏覽器裡使用,所以你可以用 ssr: false 變數來配置外掛只從客戶端還是服務端執行。

舉個栗子:

nuxt.config.js:

module.exports = {
  plugins: [
    { src: '~/plugins/vue-notifications', ssr: false }//設定ssr 為false
  ]
}
plugins/vue-notifications.js://定義一個外掛

import Vue from 'vue'
import VueNotifications from 'vue-notifications'

Vue.use(VueNotifications)

同樣地,如果有些指令碼庫你只想在服務端使用,在 Webpack 打包 server.bundle.js 檔案的時候會將 process.server 變數設定成 true

3、配置服務端 http.js 和 index.js 的內容

// http.js 封裝 axios,防止多處打包
import Axios from "axios";
import config from "~/config";//引入配置檔案

// 例項化 axios()
const http = Axios.create({
    baseURL: config.api,//根url
    timeout: 8000,
    validateStatus: function (status) {
        return status >= 200;
    }
});

// 定義錯誤異常方法
function LogicError (message, code, data) {
    this.message = message;
    this.code = code;
    this.data = data;
    this.name = "LogicError";
}

LogicError.prototype = new Error();
LogicError.prototype.constructor = LogicError;

// http 的request 請求
http.interceptors.request.use((data, headers) => {
    return data;
});

// http 的response 命令,失敗的呼叫上邊的失敗異常方法
http.interceptors.response.use(response => {
    const data = response.data;
    switch (data.success) {
        case true:
            return data.data;
        default:
            throw new LogicError(data.msg);
    }
}, err => {
    throw new LogicError("網路請求錯誤");
});

export default http;// 輸出http
// 定義 http 外掛,是一個全域性變數
import Vue from "vue";
import http from "./http.js";// 引入http封裝的axios

const install = function (VueClass, opts = {}) {

    // http method
    VueClass.http = http;
    VueClass.prototype.$http = http;
};
Vue.use(install);// 在vue 中,使用該外掛

6、在 nuxt.config.js 中引用我們的服務端外掛

這樣新增以後,我們就可以全域性使用請求命令了,打包的時候,也只會打包一個

提示:1、記得需要按照提示安裝 axios npm install --save axios 

           2、引入的元件庫必須配置 plugins, 但是有的元件庫不支援 ssr.

7、設計修改 pages 下的 index.vue 頁面,非同步獲取介面資料

 提示:這個就是文章開頭提到的問題

1、為什麼要非同步?

asyncData方法會在元件(限於頁面元件,也就是pages 資料夾下的vue檔案 )每次載入之前被呼叫。它可以在服務端或路由更新之前被呼叫。 在這個方法被呼叫的時候,第一個引數被設定為當前頁面的上下文物件,你可以利用 asyncData方法來獲取資料,Nuxt.js 會將 asyncData 返回的資料融合元件 data 方法返回的資料一併返回給當前元件。

注意:由於asyncData方法是在元件 初始化 前被呼叫的,所以在方法內是沒有辦法通過 this 來引用元件的例項物件。

Nuxt.js 提供了幾種不同的方法來使用 asyncData 方法,你可以選擇自己熟悉的一種來用:

  1. 返回一個 Promise, nuxt.js會等待該Promise被解析之後才會設定元件的資料,從而渲染元件.

返回 Promise

export default {
  asyncData ({ params }) {
    return axios.get(`https://my-api/posts/${params.id}`)
    .then((res) => {
      return { title: res.data.title }
    })
  }
}

使用 async或await

export default {
  async asyncData ({ params }) {
    let { data } = await axios.get(`https://my-api/posts/${params.id}`)
    return { title: data.title }
  }
}
<!-- index.vue 頁面 -->
<template> <div class="u-marginBottom40 js-collectionStream"> <div class="streamItem streamItem--section js-streamItem"> <div class="u-clearfix u-maxWidth1000 u-marginAuto"> <div class="row u-marginTop30 u-marginBottom20 u-sm-marginLeft20 u-sm-marginRight20 u-xs-marginTop0 u-xs-marginRight0 u-xs-marginLeft0"> <div v-for="post in blogs" :key="post.bID" class="postArticle postArticle--short is-withAccentColors"> <!--......--> </div> </div> </div> </div> </div> </template> <script> import Vue from "vue";//引入 vue 例項,獲取全域性變數 export default { layout: "blog",//這個就是我們自定義的模板佈局 async asyncData (ctx) {// asyncData() 非同步獲取資料方法 let blogs = []; try { console.log(1) const blogData = await Vue.http.get("Blog?page=1&bcategory=技術博文"); console.log(blogData); blogs = blogData; return { blogs: blogs, }; } catch (e) { //ctx.error({ statusCode: 500, message: "出錯啦" });//自定義錯誤頁面 } }, data () { return {}; }, head () {//針對每一個頁面,進行封裝 head return { meta: [ { name: "description", content: "老張的哲學是個人部落格,利用NUXT.js的服務端渲染方案" } ] }; }, mounted () {}, filters: {//過濾器,用來過濾時間字串 timeFormat: function (time) { if (!time) return ""; return time; } }, methods: {}, components: { } }; </script>

8、啟動專案,就能看到我們的資料了

提示:如果報 less 錯誤,請安裝 

  npm install less less-loader

 

三、頁面是如何一步步載入資料的 ? —— 存疑

這裡先給大家丟擲幾個問題:

1、我們通過第一次編譯的時候,生成 .nuxt 臨時資料夾,是服務端渲染還是客戶端渲染?

2、我們開啟瀏覽器的 除錯工具,發現每次修改,會都生成一些提示,當然這都是 webpack 的熱載入,那這些又是什麼含義呢?

 

3、既然是服務端和客戶端一起渲染,我們的 頁面路由 是如何匹配到的呢?

4、開啟我們的頁面 network 網路請求,發現有很多不知道的檔案,都是怎樣生成的呢?

其實這幾個問題我們在之前的都通過文字的形式說過,因為時間的問題,今天暫時就說到這裡,明天咱們再繼續深入研究這個問題,順便填充下詳情頁的資料。

四、CODE